From b67e537d3cbc222bc498aebe7953bd7b4400ac20 Mon Sep 17 00:00:00 2001 From: caixw Date: Wed, 17 Apr 2024 12:44:54 +0800 Subject: [PATCH] =?UTF-8?q?refactor(server/config):=20=E5=B0=86=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=E4=BB=8E=20server?= =?UTF-8?q?=20=E4=B8=AD=E7=8B=AC=E7=AB=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/web/go.mod | 4 +- cmd/web/go.sum | 8 +- go.mod | 4 +- go.sum | 8 +- mimetype/html/view_test.go | 13 +- mimetype/jsonp/jsonp_test.go | 4 +- mimetype/sse/client_test.go | 11 +- mimetype/sse/server_test.go | 11 +- router.go | 2 +- server/app/cli.go | 9 +- server/codec.go | 247 ------------------- server/codec_test.go | 86 ------- server/{ => config}/CONFIG.html | 5 +- server/{ => config}/cache.go | 5 +- server/{ => config}/cache_test.go | 2 +- server/config/codec.go | 109 ++++++++ server/config/codec_test.go | 83 +++++++ server/{ => config}/config.go | 148 ++++++----- server/{ => config}/config_test.go | 16 +- server/config/file.go | 59 +++++ server/{ => config}/file_test.go | 10 +- server/{ => config}/http.go | 30 ++- server/{ => config}/http_test.go | 10 +- server/{ => config}/logs.go | 188 ++++---------- server/{ => config}/logs_test.go | 18 +- server/{ => config}/micro.go | 13 +- server/{ => config}/micro_test.go | 2 +- server/{ => config}/register.go | 75 +++--- server/{ => config}/register_test.go | 6 +- server/{ => config}/testdata/cert.pem | 0 server/{ => config}/testdata/invalid-web.xml | 0 server/{ => config}/testdata/key.pem | 0 server/{ => config}/testdata/req.cnf | 0 server/{ => config}/testdata/ssh-keygen.sh | 4 +- server/{ => config}/testdata/user.xml | 0 server/{ => config}/testdata/web.json | 0 server/{ => config}/testdata/web.xml | 0 server/{ => config}/testdata/web.yaml | 0 server/file.go | 70 ------ server/options.go | 199 ++++----------- server/options_test.go | 10 +- server/registry/registry_test.go | 8 +- server/server.go | 2 - server/server_test.go | 28 +-- server/testdata/.gitignore | 1 - server/testdata/test.go | 5 - web.go | 2 +- 47 files changed, 596 insertions(+), 919 deletions(-) delete mode 100644 server/codec.go delete mode 100644 server/codec_test.go rename server/{ => config}/CONFIG.html (99%) rename server/{ => config}/cache.go (94%) rename server/{ => config}/cache_test.go (97%) create mode 100644 server/config/codec.go create mode 100644 server/config/codec_test.go rename server/{ => config}/config.go (79%) rename server/{ => config}/config_test.go (80%) create mode 100644 server/config/file.go rename server/{ => config}/file_test.go (87%) rename server/{ => config}/http.go (93%) rename server/{ => config}/http_test.go (95%) rename server/{ => config}/logs.go (63%) rename server/{ => config}/logs_test.go (82%) rename server/{ => config}/micro.go (93%) rename server/{ => config}/micro_test.go (99%) rename server/{ => config}/register.go (79%) rename server/{ => config}/register_test.go (80%) rename server/{ => config}/testdata/cert.pem (100%) rename server/{ => config}/testdata/invalid-web.xml (100%) rename server/{ => config}/testdata/key.pem (100%) rename server/{ => config}/testdata/req.cnf (100%) rename server/{ => config}/testdata/ssh-keygen.sh (77%) rename server/{ => config}/testdata/user.xml (100%) rename server/{ => config}/testdata/web.json (100%) rename server/{ => config}/testdata/web.xml (100%) rename server/{ => config}/testdata/web.yaml (100%) delete mode 100644 server/file.go delete mode 100644 server/testdata/.gitignore delete mode 100644 server/testdata/test.go diff --git a/cmd/web/go.mod b/cmd/web/go.mod index 15e63e45..427dd68d 100644 --- a/cmd/web/go.mod +++ b/cmd/web/go.mod @@ -5,10 +5,10 @@ go 1.22.0 require ( github.com/caixw/gobuild v1.7.5 github.com/getkin/kin-openapi v0.124.0 - github.com/issue9/assert/v4 v4.1.1 + github.com/issue9/assert/v4 v4.2.0 github.com/issue9/cmdopt v0.13.1 github.com/issue9/localeutil v0.26.5 - github.com/issue9/logs/v7 v7.5.1 + github.com/issue9/logs/v7 v7.6.0 github.com/issue9/mux/v8 v8.1.0 github.com/issue9/query/v3 v3.1.3 github.com/issue9/sliceutil v0.16.1 diff --git a/cmd/web/go.sum b/cmd/web/go.sum index b05bfcb8..f63c1f70 100644 --- a/cmd/web/go.sum +++ b/cmd/web/go.sum @@ -16,8 +16,8 @@ github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= -github.com/issue9/assert/v4 v4.1.1 h1:OhPE8SB8n/qZCNGLQa+6MQtr/B3oON0JAVj68k8jJlc= -github.com/issue9/assert/v4 v4.1.1/go.mod h1:v7qDRXi7AsaZZNh8eAK2rkLJg5/clztqQGA1DRv9Lv4= +github.com/issue9/assert/v4 v4.2.0 h1:XJGMFYW0xfESqFRPLWbSsr0xWdkofytvQbDfNb5n9fw= +github.com/issue9/assert/v4 v4.2.0/go.mod h1:v7qDRXi7AsaZZNh8eAK2rkLJg5/clztqQGA1DRv9Lv4= github.com/issue9/cache v0.12.0 h1:NiDBuN9x22H4UJsOMDoEuIFA8r3qNqPqO9vyzzcvzoY= github.com/issue9/cache v0.12.0/go.mod h1:0s9j7qiKv4uWYqz0D2N2H7bIBvmtD+903h5GqnxW6i4= github.com/issue9/cmdopt v0.13.1 h1:VA/Hgd92NBbZyHjZx1xcRCMhoc+XjI1LWhiuZkOZ0VU= @@ -30,8 +30,8 @@ github.com/issue9/errwrap v0.3.2 h1:7KEme9Pfe75M+sIMcPCn/DV90wjnOcRbO4DXVAHj3Fw= github.com/issue9/errwrap v0.3.2/go.mod h1:KcCLuUGiffjooLCUjL89r1cyO8/HT/VRcQrneO53N3A= github.com/issue9/localeutil v0.26.5 h1:e78b6cOOtgzfb4g4U9uPLC8QyK6Lux+s7ZiQe+6iM1A= github.com/issue9/localeutil v0.26.5/go.mod h1:BJXJwcAT9CyyVZOlqfmq+B5FcPbqGxGjYnTYbVuiMM8= -github.com/issue9/logs/v7 v7.5.1 h1:H1ua+3C0Nm6LZt4gEFhKZiyHhzyYEkPVYJDtt+1jmvQ= -github.com/issue9/logs/v7 v7.5.1/go.mod h1:UA05C4wF8vrrQp13QV1ncNqI/6nJ8R1c/dpOOcbSKFQ= +github.com/issue9/logs/v7 v7.6.0 h1:dvY1ctPROdd2YaOwYRNOkfbmMx+8OM0w53t8bWrWg9s= +github.com/issue9/logs/v7 v7.6.0/go.mod h1:7Hx1vnAojUciyFdqNlMiwsBJRGBc/P2Yrjt7ACm9Uno= github.com/issue9/mux/v8 v8.1.0 h1:NZmQv0iE0ocn1oyHWKZWY+PIVcbpZYdaDImJ3+WJ/28= github.com/issue9/mux/v8 v8.1.0/go.mod h1:ivUHUcMzoTPxvjC33XrsWJ68fnw5vEjJ0y1GcuMrg84= github.com/issue9/query/v3 v3.1.3 h1:Y6ETEYXxaKqhpM4lXPKCffhJ72VuKQbrAwgwHlacu0Y= diff --git a/go.mod b/go.mod index c5770d69..88453a8d 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,13 @@ go 1.22.0 require ( github.com/andybalholm/brotli v1.1.0 github.com/fxamacker/cbor/v2 v2.6.0 - github.com/issue9/assert/v4 v4.1.1 + github.com/issue9/assert/v4 v4.2.0 github.com/issue9/cache v0.12.0 github.com/issue9/config v0.6.2 github.com/issue9/conv v1.3.5 github.com/issue9/errwrap v0.3.2 github.com/issue9/localeutil v0.26.5 - github.com/issue9/logs/v7 v7.5.1 + github.com/issue9/logs/v7 v7.6.0 github.com/issue9/mux/v8 v8.1.0 github.com/issue9/query/v3 v3.1.3 github.com/issue9/scheduled v0.19.5 diff --git a/go.sum b/go.sum index fbbed046..09e60d37 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA= github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/issue9/assert/v4 v4.1.1 h1:OhPE8SB8n/qZCNGLQa+6MQtr/B3oON0JAVj68k8jJlc= -github.com/issue9/assert/v4 v4.1.1/go.mod h1:v7qDRXi7AsaZZNh8eAK2rkLJg5/clztqQGA1DRv9Lv4= +github.com/issue9/assert/v4 v4.2.0 h1:XJGMFYW0xfESqFRPLWbSsr0xWdkofytvQbDfNb5n9fw= +github.com/issue9/assert/v4 v4.2.0/go.mod h1:v7qDRXi7AsaZZNh8eAK2rkLJg5/clztqQGA1DRv9Lv4= github.com/issue9/cache v0.12.0 h1:NiDBuN9x22H4UJsOMDoEuIFA8r3qNqPqO9vyzzcvzoY= github.com/issue9/cache v0.12.0/go.mod h1:0s9j7qiKv4uWYqz0D2N2H7bIBvmtD+903h5GqnxW6i4= github.com/issue9/config v0.6.2 h1:znXvsk6gh0wm+fTEn0zUjjramKuOLY8Jt0ZTxp4GIkc= @@ -26,8 +26,8 @@ github.com/issue9/errwrap v0.3.2 h1:7KEme9Pfe75M+sIMcPCn/DV90wjnOcRbO4DXVAHj3Fw= github.com/issue9/errwrap v0.3.2/go.mod h1:KcCLuUGiffjooLCUjL89r1cyO8/HT/VRcQrneO53N3A= github.com/issue9/localeutil v0.26.5 h1:e78b6cOOtgzfb4g4U9uPLC8QyK6Lux+s7ZiQe+6iM1A= github.com/issue9/localeutil v0.26.5/go.mod h1:BJXJwcAT9CyyVZOlqfmq+B5FcPbqGxGjYnTYbVuiMM8= -github.com/issue9/logs/v7 v7.5.1 h1:H1ua+3C0Nm6LZt4gEFhKZiyHhzyYEkPVYJDtt+1jmvQ= -github.com/issue9/logs/v7 v7.5.1/go.mod h1:UA05C4wF8vrrQp13QV1ncNqI/6nJ8R1c/dpOOcbSKFQ= +github.com/issue9/logs/v7 v7.6.0 h1:dvY1ctPROdd2YaOwYRNOkfbmMx+8OM0w53t8bWrWg9s= +github.com/issue9/logs/v7 v7.6.0/go.mod h1:7Hx1vnAojUciyFdqNlMiwsBJRGBc/P2Yrjt7ACm9Uno= github.com/issue9/mux/v8 v8.1.0 h1:NZmQv0iE0ocn1oyHWKZWY+PIVcbpZYdaDImJ3+WJ/28= github.com/issue9/mux/v8 v8.1.0/go.mod h1:ivUHUcMzoTPxvjC33XrsWJ68fnw5vEjJ0y1GcuMrg84= github.com/issue9/query/v3 v3.1.3 h1:Y6ETEYXxaKqhpM4lXPKCffhJ72VuKQbrAwgwHlacu0Y= diff --git a/mimetype/html/view_test.go b/mimetype/html/view_test.go index 3ad74da6..36ee1fbe 100644 --- a/mimetype/html/view_test.go +++ b/mimetype/html/view_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/issue9/assert/v4" + "github.com/issue9/logs/v7" "github.com/issue9/mux/v8/header" "golang.org/x/text/language" @@ -24,16 +25,8 @@ func newServer(a *assert.Assertion, lang string) web.Server { s, err := server.New("test", "1.0.0", &server.Options{ HTTPServer: &http.Server{Addr: ":8080"}, Language: language.MustParse(lang), - Mimetypes: []*server.Mimetype{ - { - Name: html.Mimetype, - Marshal: html.Marshal, - Unmarshal: html.Unmarshal, - }, - }, - Logs: &server.Logs{ - Handler: server.NewTermHandler(os.Stderr, nil), - }, + Codec: web.NewCodec().AddMimetype(html.Mimetype, html.Marshal, html.Unmarshal, ""), + Logs: logs.New(logs.NewTermHandler(os.Stderr, nil)), }) a.NotError(err).NotNil(s) diff --git a/mimetype/jsonp/jsonp_test.go b/mimetype/jsonp/jsonp_test.go index fe602644..efa6712f 100644 --- a/mimetype/jsonp/jsonp_test.go +++ b/mimetype/jsonp/jsonp_test.go @@ -18,9 +18,7 @@ import ( func TestJSONP(t *testing.T) { a := assert.New(t, false) s, err := server.New("test", "1.0.0", &server.Options{ - Mimetypes: []*server.Mimetype{ - {Name: Mimetype, Marshal: Marshal, Unmarshal: Unmarshal, Problem: ""}, - }, + Codec: web.NewCodec().AddMimetype(Mimetype, Marshal, Unmarshal, ""), HTTPServer: &http.Server{Addr: ":8080"}, }) a.NotError(err).NotNil(s) diff --git a/mimetype/sse/client_test.go b/mimetype/sse/client_test.go index 63a90971..fde0dd05 100644 --- a/mimetype/sse/client_test.go +++ b/mimetype/sse/client_test.go @@ -14,6 +14,7 @@ import ( "time" "github.com/issue9/assert/v4" + "github.com/issue9/logs/v7" "github.com/issue9/web" "github.com/issue9/web/mimetype/nop" @@ -56,14 +57,8 @@ func TestOnMessage(t *testing.T) { a := assert.New(t, false) s, err := server.New("test", "1.0.0", &server.Options{ HTTPServer: &http.Server{Addr: ":8080"}, - Mimetypes: []*server.Mimetype{ - {Name: Mimetype, Marshal: nop.Marshal, Unmarshal: nop.Unmarshal}, - }, - Logs: &server.Logs{ - Created: server.MicroLayout, - Handler: server.NewTermHandler(os.Stderr, nil), - Levels: server.AllLevels(), - }, + Codec: web.NewCodec().AddMimetype(Mimetype, nop.Marshal, nop.Unmarshal, ""), + Logs: logs.New(logs.NewTermHandler(os.Stderr, nil), logs.WithCreated(logs.MicroLayout)), }) a.NotError(err).NotNil(s) e := NewServer[int64](s, 50*time.Millisecond, 5*time.Second, 10) diff --git a/mimetype/sse/server_test.go b/mimetype/sse/server_test.go index 9208fdb2..eaa51fe0 100644 --- a/mimetype/sse/server_test.go +++ b/mimetype/sse/server_test.go @@ -13,6 +13,7 @@ import ( "time" "github.com/issue9/assert/v4" + "github.com/issue9/logs/v7" "github.com/issue9/mux/v8/header" "github.com/issue9/web" @@ -25,14 +26,8 @@ func TestServer(t *testing.T) { a := assert.New(t, false) s, err := server.New("test", "1.0.0", &server.Options{ HTTPServer: &http.Server{Addr: ":8080"}, - Mimetypes: []*server.Mimetype{ - {Name: header.JSON, Marshal: json.Marshal, Unmarshal: json.Unmarshal}, - }, - Logs: &server.Logs{ - Created: server.MicroLayout, - Handler: server.NewTermHandler(os.Stderr, nil), - Levels: server.AllLevels(), - }, + Codec: web.NewCodec().AddMimetype(json.Mimetype, json.Marshal, json.Unmarshal, ""), + Logs: logs.New(logs.NewTermHandler(os.Stderr, nil), logs.WithCreated(logs.MicroLayout), logs.WithLevels(logs.AllLevels()...)), }) a.NotError(err).NotNil(s) e := NewServer[int64](s, 50*time.Millisecond, 5*time.Second, 10) diff --git a/router.go b/router.go index c75664e1..cad564c2 100644 --- a/router.go +++ b/router.go @@ -102,7 +102,7 @@ func Recovery(status int, l *Logger) RouterOption { return mux.Recovery(func(w http.ResponseWriter, msg any) { err, ok := msg.(error) if !ok { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + http.Error(w, http.StatusText(status), status) l.String(source.Stack(4, err)) return } diff --git a/server/app/cli.go b/server/app/cli.go index 52849fdf..4b396168 100644 --- a/server/app/cli.go +++ b/server/app/cli.go @@ -18,6 +18,7 @@ import ( "github.com/issue9/web" "github.com/issue9/web/locales" "github.com/issue9/web/server" + "github.com/issue9/web/server/config" ) const ( @@ -64,7 +65,7 @@ type CLIOptions[T any] struct { // // 相对于 ConfigDir 的文件名,不能为空。 // - // 需要保证序列化方法已经由 [RegisterFileSerializer] 注册; + // 需要保证序列化方法已经由 [config.RegisterFileSerializer] 注册; ConfigFilename string // 本地化的打印对象 @@ -101,7 +102,7 @@ type cli[T any] struct { // - -h 显示帮助信息; // - -a 执行的指令,该值会传递给 [CLIOptions.NewServer],由用户根据此值决定初始化方式; // -// T 表示的是配置文件中的用户自定义数据类型,可参考 [server.LoadOptions] 中有关 User 的说明。 +// T 表示的是配置文件中的用户自定义数据类型,可参考 [config.Load] 中有关 User 的说明。 // // 如果是 [CLIOptions] 本身字段设置有问题会直接 panic。 func NewCLI[T comparable](o *CLIOptions[T]) App { @@ -112,7 +113,7 @@ func NewCLI[T comparable](o *CLIOptions[T]) App { var action string // -a 参数 initServer := func() (web.Server, error) { - opt, user, err := server.LoadOptions[T](o.ConfigDir, o.ConfigFilename) + opt, user, err := config.Load[T](o.ConfigDir, o.ConfigFilename) if err != nil { return nil, web.NewStackError(err) } @@ -167,7 +168,7 @@ func (cmd *cli[T]) Exec() error { return cmd.exec(os.Args) } func (o *CLIOptions[T]) sanitize() error { if o.Printer == nil { - p, err := server.NewPrinter("*.yaml", locales.Locales...) + p, err := config.NewPrinter("*.yaml", locales.Locales...) if err != nil { return err } diff --git a/server/codec.go b/server/codec.go deleted file mode 100644 index 2594483b..00000000 --- a/server/codec.go +++ /dev/null @@ -1,247 +0,0 @@ -// SPDX-FileCopyrightText: 2018-2024 caixw -// -// SPDX-License-Identifier: MIT - -package server - -import ( - "compress/flate" - "compress/gzip" - "compress/lzw" - "strconv" - - "github.com/andybalholm/brotli" - "github.com/issue9/sliceutil" - - "github.com/issue9/web" - "github.com/issue9/web/compressor" - "github.com/issue9/web/locales" - "github.com/issue9/web/mimetype/json" - "github.com/issue9/web/mimetype/xml" -) - -// Compression 有关压缩的设置项 -type Compression struct { - // Compressor 压缩算法 - Compressor compressor.Compressor - - // Types 该压缩对象允许使用的为 content-type 类型 - // - // 如果是 * 或是空值表示适用所有类型。 - Types []string -} - -// Mimetype 有关 mimetype 的设置项 -type Mimetype struct { - // Mimetype 的名称 - // - // 比如:application/json - Name string - - // 对应的错误状态下的 mimetype 值 - // - // 比如:application/problem+json。 - // 可以为空,表示与 Type 相同。 - Problem string - - // 生成编码方法 - Marshal web.MarshalFunc - - // 解码方法 - Unmarshal web.UnmarshalFunc -} - -type compressConfig struct { - // Type content-type 的值 - // - // 可以带通配符,比如 text/* 表示所有 text/ 开头的 content-type 都采用此压缩方法。 - Types []string `json:"types" xml:"type" yaml:"types"` - - // IDs 压缩方法的 ID 列表 - // - // 这些 ID 值必须是由 [RegisterCompress] 注册的,否则无效,默认情况下支持以下类型: - // - deflate-default - // - deflate-best-compression - // - deflate-best-speed - // - gzip-default - // - gzip-best-compression - // - gzip-best-speed - // - compress-lsb-8 - // - compress-msb-8 - // - br-default - // - br-best-compression - // - br-best-speed - // - zstd-default - ID string `json:"id" xml:"id,attr" yaml:"id"` -} - -type mimetypeConfig struct { - // 编码名称 - // - // 比如 application/xml 等 - Type string `json:"type" yaml:"type" xml:"type,attr"` - - // 返回错误代码是的 mimetype - // - // 比如正常情况下如果是 application/json,那么此值可以是 application/problem+json。 - // 如果为空,表示与 Type 相同。 - Problem string `json:"problem,omitempty" yaml:"problem,omitempty" xml:"problem,attr,omitempty"` - - // 实际采用的解码方法 - // - // 由 [RegisterMimetype] 注册而来。默认可用为: - // - // - xml - // - cbor - // - json - // - form - // - html - // - gob - // - nop 没有具体实现的方法,对于上传等需要自行处理的情况可以指定此值。 - Target string `json:"target" yaml:"target" xml:"target,attr"` -} - -func buildCodec(ms []*Mimetype, cs []*Compression) (*web.Codec, *web.FieldError) { - if len(ms) == 0 { - ms = JSONMimetypes() - } - - // 检测是否存在同名的项 - indexes := sliceutil.Dup(ms, func(e1, e2 *Mimetype) bool { return e1.Name == e2.Name }) - if len(indexes) > 0 { - return nil, web.NewFieldError("Mimetypes["+strconv.Itoa(indexes[0])+"].Name", locales.DuplicateValue) - } - - c := web.NewCodec() - - for i, s := range ms { - if s.Name == "" { - return nil, web.NewFieldError("Mimetypes["+strconv.Itoa(i)+"].Name", locales.CanNotBeEmpty) - } - - if s.Marshal == nil { - return nil, web.NewFieldError("Mimetypes["+strconv.Itoa(i)+"].Marshal", locales.CanNotBeEmpty) - } - - if s.Unmarshal == nil { - return nil, web.NewFieldError("Mimetypes["+strconv.Itoa(i)+"].Unmarshal", locales.CanNotBeEmpty) - } - - c.AddMimetype(s.Name, s.Marshal, s.Unmarshal, s.Problem) - } - - for i, s := range cs { - if s.Compressor == nil { - return nil, web.NewFieldError("Compressions["+strconv.Itoa(i)+"].Compressor", locales.CanNotBeEmpty) - } - c.AddCompressor(s.Compressor, s.Types...) - } - - return c, nil -} - -// DefaultCompressions 提供当前框架内置的所有压缩算法 -// -// contentType 指定所有算法应用的媒体类型,为空则表示对所有的内容都进行压缩。 -func DefaultCompressions(contentType ...string) []*Compression { - return []*Compression{ - {Compressor: compressor.NewGzip(gzip.DefaultCompression), Types: contentType}, - {Compressor: compressor.NewDeflate(flate.DefaultCompression, nil), Types: contentType}, - {Compressor: compressor.NewLZW(lzw.LSB, 8), Types: contentType}, - {Compressor: compressor.NewBrotli(brotli.WriterOptions{}), Types: contentType}, - {Compressor: compressor.NewZstd(), Types: contentType}, - } -} - -// BestSpeedCompressions 提供当前框架内置的所有压缩算法 -// -// 如果有性能参数,则选择最快速度作为初始化条件。 -func BestSpeedCompressions(contentType ...string) []*Compression { - return []*Compression{ - {Compressor: compressor.NewGzip(gzip.BestSpeed), Types: contentType}, - {Compressor: compressor.NewDeflate(flate.BestSpeed, nil), Types: contentType}, - {Compressor: compressor.NewLZW(lzw.LSB, 8), Types: contentType}, - {Compressor: compressor.NewBrotli(brotli.WriterOptions{Quality: brotli.BestSpeed}), Types: contentType}, - {Compressor: compressor.NewZstd(), Types: contentType}, - } -} - -// BestCompressionCompressions 提供当前框架内置的所有压缩算法 -// -// 如果有性能参数,则选择最快压缩比作为初始化条件。 -func BestCompressionCompressions(contentType ...string) []*Compression { - return []*Compression{ - {Compressor: compressor.NewGzip(gzip.BestCompression), Types: contentType}, - {Compressor: compressor.NewDeflate(flate.BestCompression, nil), Types: contentType}, - {Compressor: compressor.NewLZW(lzw.LSB, 8), Types: contentType}, - {Compressor: compressor.NewBrotli(brotli.WriterOptions{Quality: brotli.BestCompression}), Types: contentType}, - {Compressor: compressor.NewZstd(), Types: contentType}, - } -} - -// APIMimetypes 返回以 XML 和 JSON 作为数据交换格式的配置项 -func APIMimetypes() []*Mimetype { - return []*Mimetype{ - {Name: json.Mimetype, Marshal: json.Marshal, Unmarshal: json.Unmarshal, Problem: json.ProblemMimetype}, - {Name: xml.Mimetype, Marshal: xml.Marshal, Unmarshal: xml.Unmarshal, Problem: xml.ProblemMimetype}, - } -} - -// XMLMimetypes 返回以 XML 作为数据交换格式的配置项 -func XMLMimetypes() []*Mimetype { - return []*Mimetype{ - {Name: xml.Mimetype, Marshal: xml.Marshal, Unmarshal: xml.Unmarshal, Problem: xml.ProblemMimetype}, - } -} - -// JSONMimetypes 返回以 JSON 作为数据交换格式的配置项 -func JSONMimetypes() []*Mimetype { - return []*Mimetype{ - {Name: json.Mimetype, Marshal: json.Marshal, Unmarshal: json.Unmarshal, Problem: json.ProblemMimetype}, - } -} - -func (conf *configOf[T]) sanitizeCompresses() *web.FieldError { - conf.compressors = make([]*Compression, 0, len(conf.Compressors)) - for index, e := range conf.Compressors { - enc, found := compressorFactory.get(e.ID) - if !found { - field := "compresses[" + strconv.Itoa(index) + "].id" - return web.NewFieldError(field, locales.ErrNotFound()) - } - - conf.compressors = append(conf.compressors, &Compression{ - Compressor: enc, - Types: e.Types, - }) - } - return nil -} - -func (conf *configOf[T]) sanitizeMimetypes() *web.FieldError { - indexes := sliceutil.Dup(conf.Mimetypes, func(i, j *mimetypeConfig) bool { return i.Type == j.Type }) - if len(indexes) > 0 { - value := conf.Mimetypes[indexes[1]].Type - err := web.NewFieldError("mimetypes["+strconv.Itoa(indexes[1])+"].target", locales.DuplicateValue) - err.Value = value - return err - } - - ms := make([]*Mimetype, 0, len(conf.Mimetypes)) - for index, item := range conf.Mimetypes { - m, found := mimetypesFactory.get(item.Target) - if !found { - return web.NewFieldError("mimetypes["+strconv.Itoa(index)+"].target", locales.ErrNotFound()) - } - - ms = append(ms, &Mimetype{ - Marshal: m.marshal, - Unmarshal: m.unmarshal, - Name: item.Type, - Problem: item.Problem, - }) - } - conf.mimetypes = ms - - return nil -} diff --git a/server/codec_test.go b/server/codec_test.go deleted file mode 100644 index 2e08394d..00000000 --- a/server/codec_test.go +++ /dev/null @@ -1,86 +0,0 @@ -// SPDX-FileCopyrightText: 2018-2024 caixw -// -// SPDX-License-Identifier: MIT - -package server - -import ( - "compress/lzw" - "testing" - - "github.com/issue9/assert/v4" - - "github.com/issue9/web/compressor" - "github.com/issue9/web/mimetype/json" -) - -func TestBuildCodec(t *testing.T) { - a := assert.New(t, false) - - c, err := buildCodec(nil, nil) - a.NotError(err).NotNil(c) - - c, err = buildCodec(APIMimetypes(), DefaultCompressions()) - a.NotError(err).NotNil(c) - - c, err = buildCodec([]*Mimetype{ - {Name: "application", Marshal: nil, Unmarshal: nil}, - {Name: "nil", Marshal: nil, Unmarshal: nil}, - {Name: "nil", Marshal: nil, Unmarshal: nil}, - }, BestCompressionCompressions()) - a.NotNil(err).Equal(err.Field, "Mimetypes[1].Name").Nil(c) - - c, err = buildCodec([]*Mimetype{ - {Name: "", Marshal: nil, Unmarshal: nil}, - }, BestSpeedCompressions()) - a.NotNil(err).Equal(err.Field, "Mimetypes[0].Name").Nil(c) - - c, err = buildCodec([]*Mimetype{ - {Name: "text", Marshal: nil, Unmarshal: nil}, - }, BestSpeedCompressions()) - a.NotNil(err).Equal(err.Field, "Mimetypes[0].Marshal").Nil(c) - - c, err = buildCodec([]*Mimetype{ - {Name: "text", Marshal: json.Marshal, Unmarshal: nil}, - }, BestSpeedCompressions()) - a.NotNil(err).Equal(err.Field, "Mimetypes[0].Unmarshal").Nil(c) - - c, err = buildCodec(XMLMimetypes(), []*Compression{ - {Compressor: compressor.NewLZW(lzw.LSB, 8)}, - {Compressor: nil}, - }) - a.Equal(err.Field, "Compressions[1].Compressor").Nil(c) -} - -func TestConfigOf_sanitizeCompresses(t *testing.T) { - a := assert.New(t, false) - - conf := &configOf[empty]{Compressors: []*compressConfig{ - {Types: []string{"text/*", "application/*"}, ID: "compress-msb-8"}, - {Types: []string{"text/*"}, ID: "br-default"}, - {Types: []string{"application/*"}, ID: "gzip-default"}, - }} - a.NotError(conf.sanitizeCompresses()) - - conf = &configOf[empty]{Compressors: []*compressConfig{ - {Types: []string{"text/*"}, ID: "compress-msb-8"}, - {Types: []string{"text/*"}, ID: "not-exists-id"}, - }} - err := conf.sanitizeCompresses() - a.Error(err).Equal(err.Field, "compresses[1].id") -} - -func TestRegisterMimetype(t *testing.T) { - a := assert.New(t, false) - - v, f := mimetypesFactory.get("json") - a.True(f). - NotNil(v). - NotNil(v.marshal) - - RegisterMimetype(nil, nil, "json") - v, f = mimetypesFactory.get("json") - a.True(f). - NotNil(v). - Nil(v.marshal) -} diff --git a/server/CONFIG.html b/server/config/CONFIG.html similarity index 99% rename from server/CONFIG.html rename to server/config/CONFIG.html index be8d09a9..2f295ae9 100644 --- a/server/CONFIG.html +++ b/server/config/CONFIG.html @@ -61,8 +61,6 @@

config

configOf

在项目正式运行

为空和 Local(注意大小写) 值都会被初始化本地时间。 cache,omitemptycache,omitemptycache,omitemptycacheConfig

指定缓存对象

如果为空,则会采用内存作为缓存对象。 -compressions,omitemptycompressions,omitemptycompressions>compression,omitemptycompressConfig

压缩的相关配置 -

如果为空,那么不支持压缩功能。 fileSerializers,omitemptyfileSerializers,omitemptyfileSerializers>fileSerializer,omitemptystring

指定配置文件的序列化

可通过 [RegisterFileSerializer] 进行添加额外的序列化方法。默认可用为:

如果为空,表示支持以上所有格式。 +compressions,omitemptycompressions,omitemptycompressions>compression,omitemptycompressConfig

压缩的相关配置 +

如果为空,那么不支持压缩功能。 mimetypes,omitemptymimetypes,omitemptymimetypes>mimetype,omitemptymimetypeConfig

指定可用的 mimetype

如果为空,那么将不支持任何格式的内容输出。 idGenerator,omitemptyidGenerator,omitemptyidGenerator,omitemptystring

唯一 ID 生成器 @@ -103,6 +103,7 @@

logsConfig

levels,omitemptylevels,omitemptylevel,omitemptylogs.Level

允许开启的通道

为空表示采用 [AllLevels] std,omitemptystd,omitemptystd,attr,omitemptybool

是否接管标准库的日志 +stackError,omitemptystackError,omitemptystackError,attr,omitemptybool

是否显示错误日志的调用堆栈 handlershandlershandlers>handlerlogHandlerConfig

日志输出对象的配置

为空表示 [NewNopHandler] 返回的对象。 diff --git a/server/cache.go b/server/config/cache.go similarity index 94% rename from server/cache.go rename to server/config/cache.go index 3f36ad47..8d6383e7 100644 --- a/server/cache.go +++ b/server/config/cache.go @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: MIT -package server +package config import ( "time" @@ -12,6 +12,7 @@ import ( "github.com/issue9/web" "github.com/issue9/web/locales" + "github.com/issue9/web/server" ) // CacheBuilder 构建缓存客户端的方法 @@ -61,7 +62,7 @@ func (conf *configOf[T]) buildCache() *web.FieldError { } conf.cache = drv if job != nil { - conf.init = append(conf.init, func(o *Options) { + conf.init = append(conf.init, func(o *server.Options) { o.Init = append(o.Init, func(s web.Server) { s.Services().AddTicker(locales.RecycleLocalCache, job.Job, job.Ticker, false, false) }) diff --git a/server/cache_test.go b/server/config/cache_test.go similarity index 97% rename from server/cache_test.go rename to server/config/cache_test.go index 48721a91..0a0c0d38 100644 --- a/server/cache_test.go +++ b/server/config/cache_test.go @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: MIT -package server +package config import ( "testing" diff --git a/server/config/codec.go b/server/config/codec.go new file mode 100644 index 00000000..00e22ab7 --- /dev/null +++ b/server/config/codec.go @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: 2018-2024 caixw +// +// SPDX-License-Identifier: MIT + +package config + +import ( + "strconv" + + "github.com/issue9/sliceutil" + + "github.com/issue9/web" + "github.com/issue9/web/locales" +) + +type compressConfig struct { + // Type content-type 的值 + // + // 可以带通配符,比如 text/* 表示所有 text/ 开头的 content-type 都采用此压缩方法。 + Types []string `json:"types" xml:"type" yaml:"types"` + + // IDs 压缩方法的 ID 列表 + // + // 这些 ID 值必须是由 [RegisterCompress] 注册的,否则无效,默认情况下支持以下类型: + // - deflate-default + // - deflate-best-compression + // - deflate-best-speed + // - gzip-default + // - gzip-best-compression + // - gzip-best-speed + // - compress-lsb-8 + // - compress-msb-8 + // - br-default + // - br-best-compression + // - br-best-speed + // - zstd-default + ID string `json:"id" xml:"id,attr" yaml:"id"` +} + +type mimetypeConfig struct { + // 编码名称 + // + // 比如 application/xml 等 + Type string `json:"type" yaml:"type" xml:"type,attr"` + + // 返回错误代码是的 mimetype + // + // 比如正常情况下如果是 application/json,那么此值可以是 application/problem+json。 + // 如果为空,表示与 Type 相同。 + Problem string `json:"problem,omitempty" yaml:"problem,omitempty" xml:"problem,attr,omitempty"` + + // 实际采用的解码方法 + // + // 由 [RegisterMimetype] 注册而来。默认可用为: + // + // - xml + // - cbor + // - json + // - form + // - html + // - gob + // - nop 没有具体实现的方法,对于上传等需要自行处理的情况可以指定此值。 + Target string `json:"target" yaml:"target" xml:"target,attr"` +} + +type mimetype struct { + marshal web.MarshalFunc + unmarshal web.UnmarshalFunc +} + +func (conf *configOf[T]) buildCodec() *web.FieldError { + if len(conf.Compressors) == 0 && len(conf.Mimetypes) == 0 { + return nil + } + + c := web.NewCodec() + + for index, e := range conf.Compressors { + enc, found := compressorFactory.get(e.ID) + if !found { + field := "compresses[" + strconv.Itoa(index) + "].id" + return web.NewFieldError(field, locales.ErrNotFound()) + } + + c.AddCompressor(enc, e.Types...) + } + + return conf.sanitizeMimetypes(c) +} + +func (conf *configOf[T]) sanitizeMimetypes(c *web.Codec) *web.FieldError { + if indexes := sliceutil.Dup(conf.Mimetypes, func(i, j *mimetypeConfig) bool { return i.Type == j.Type }); len(indexes) > 0 { + value := conf.Mimetypes[indexes[1]].Type + err := web.NewFieldError("mimetypes["+strconv.Itoa(indexes[1])+"].type", locales.DuplicateValue) + err.Value = value + return err + } + + for index, item := range conf.Mimetypes { + m, found := mimetypesFactory.get(item.Target) + if !found { + return web.NewFieldError("mimetypes["+strconv.Itoa(index)+"].target", locales.ErrNotFound()) + } + + c.AddMimetype(item.Type, m.marshal, m.unmarshal, item.Problem) + } + + return nil +} diff --git a/server/config/codec_test.go b/server/config/codec_test.go new file mode 100644 index 00000000..4e640fda --- /dev/null +++ b/server/config/codec_test.go @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2018-2024 caixw +// +// SPDX-License-Identifier: MIT + +package config + +import ( + "testing" + + "github.com/issue9/assert/v4" + + "github.com/issue9/web/locales" + "github.com/issue9/web/mimetype/json" +) + +func TestConfigOf_buildCodec(t *testing.T) { + a := assert.New(t, false) + + conf := &configOf[empty]{ + Compressors: []*compressConfig{ + {Types: []string{"text/*", "application/*"}, ID: "compress-msb-8"}, + {Types: []string{"text/*"}, ID: "br-default"}, + {Types: []string{"application/*"}, ID: "gzip-default"}, + }, + } + a.NotError(conf.buildCodec()) + + conf = &configOf[empty]{ + Compressors: []*compressConfig{ + {Types: []string{"text/*"}, ID: "compress-msb-8"}, + {Types: []string{"text/*"}, ID: "not-exists-id"}, + }, + } + err := conf.buildCodec() + a.Error(err).Equal(err.Field, "compresses[1].id") +} + +func TestConfigOf_sanitizeMimetypes(t *testing.T) { + a := assert.New(t, false) + + conf := &configOf[empty]{ + Mimetypes: []*mimetypeConfig{ + {Type: "json", Target: "json"}, + {Type: "xml", Target: "xml"}, + }, + } + a.NotError(conf.buildCodec()) + + conf = &configOf[empty]{ + Mimetypes: []*mimetypeConfig{ + {Type: "json", Target: "json"}, + {Type: "json", Target: "xml"}, + }, + } + err := conf.buildCodec() + a.Error(err).Equal(err.Field, "mimetypes[1].type") + + conf = &configOf[empty]{ + Mimetypes: []*mimetypeConfig{ + {Type: "json", Target: "json"}, + {Type: "xml", Target: "not-exists"}, + }, + } + err = conf.buildCodec() + a.Error(err). + Equal(err.Field, "mimetypes[1].target"). + Equal(err.Message, locales.ErrNotFound()) +} + +func TestRegisterMimetype(t *testing.T) { + a := assert.New(t, false) + + v, f := mimetypesFactory.get("json") + a.True(f). + NotNil(v). + NotNil(v.marshal) + + RegisterMimetype(json.Marshal, json.Unmarshal, "json") + v, f = mimetypesFactory.get("json") + a.True(f). + NotNil(v). + NotNil(v.marshal) +} diff --git a/server/config.go b/server/config/config.go similarity index 79% rename from server/config.go rename to server/config/config.go index ef9a11af..6cb5bf03 100644 --- a/server/config.go +++ b/server/config/config.go @@ -2,19 +2,29 @@ // // SPDX-License-Identifier: MIT -package server +//go:generate web htmldoc -lang=zh-CN -dir=./ -o=./CONFIG.html -object=configOf + +// Package config 从配置文件加载 [server.Options] +package config import ( + "io/fs" + "log" "runtime/debug" "time" "github.com/issue9/cache" "github.com/issue9/config" + "github.com/issue9/localeutil" + "github.com/issue9/mux/v8/group" "golang.org/x/text/language" + "golang.org/x/text/message/catalog" "github.com/issue9/web" + "github.com/issue9/web/internal/locale" "github.com/issue9/web/locales" "github.com/issue9/web/selector" + "github.com/issue9/web/server" ) // 在项目正式运行之后,对于配置项的修改应该慎之又慎, @@ -23,6 +33,8 @@ import ( type configOf[T comparable] struct { XMLName struct{} `yaml:"-" json:"-" xml:"web"` + dir string + // 内存限制 // // 如果小于等于 0,表示不设置该值。 @@ -62,12 +74,6 @@ type configOf[T comparable] struct { Cache *cacheConfig `yaml:"cache,omitempty" json:"cache,omitempty" xml:"cache,omitempty"` cache cache.Driver - // 压缩的相关配置 - // - // 如果为空,那么不支持压缩功能。 - Compressors []*compressConfig `yaml:"compressions,omitempty" json:"compressions,omitempty" xml:"compressions>compression,omitempty"` - compressors []*Compression - // 指定配置文件的序列化 // // 可通过 [RegisterFileSerializer] 进行添加额外的序列化方法。默认可用为: @@ -77,13 +83,19 @@ type configOf[T comparable] struct { // // 如果为空,表示支持以上所有格式。 FileSerializers []string `yaml:"fileSerializers,omitempty" json:"fileSerializers,omitempty" xml:"fileSerializers>fileSerializer,omitempty"` - config *Config + config *config.Config + + // 压缩的相关配置 + // + // 如果为空,那么不支持压缩功能。 + Compressors []*compressConfig `yaml:"compressions,omitempty" json:"compressions,omitempty" xml:"compressions>compression,omitempty"` // 指定可用的 mimetype // // 如果为空,那么将不支持任何格式的内容输出。 Mimetypes []*mimetypeConfig `yaml:"mimetypes,omitempty" json:"mimetypes,omitempty" xml:"mimetypes>mimetype,omitempty"` - mimetypes []*Mimetype + + codec *web.Codec // 唯一 ID 生成器 // @@ -93,7 +105,7 @@ type configOf[T comparable] struct { // - number 数值格式; // NOTE: 一旦运行在生产环境,就不应该修改此属性,除非能确保新的函数生成的 ID 不与之前生成的 ID 重复。 IDGenerator string `yaml:"idGenerator,omitempty" json:"idGenerator,omitempty" xml:"idGenerator,omitempty"` - idGenerator IDGenerator + idGenerator func() string // Problem 中 type 字段的前缀 ProblemTypePrefix string `yaml:"problemTypePrefix,omitempty" json:"problemTypePrefix,omitempty" xml:"problemTypePrefix,omitempty"` @@ -117,20 +129,20 @@ type configOf[T comparable] struct { // // NOTE: 作为微服务的网关时才会有效果 Mappers []*mapperConfig `yaml:"mappers,omitempty" json:"mappers,omitempty" xml:"mappers>mapper,omitempty"` - mapper Mapper + mapper map[string]group.Matcher // 用户自定义的配置项 User T `yaml:"user,omitempty" json:"user,omitempty" xml:"user,omitempty"` // 由其它选项生成的初始化方法 - init []func(*Options) + init []func(*server.Options) } -// LoadOptions 从配置文件初始化 [Options] 对象 +// Load 从配置文件初始化 [server.Options] 对象 // // configDir 项目配置文件所在的目录; // filename 用于指定项目的配置文件,相对于 configDir 文件系统。 -// 如果此值为空,将返回 &Options{Config: &Config{Dir: configDir}}; +// 如果此值为空,将返回 &Options{Config: config.Dir(nil, configDir)}; // // 序列化方法由 [RegisterFileSerializer] 注册的列表中根据 filename 的扩展名进行查找。 // @@ -150,10 +162,10 @@ type configOf[T comparable] struct { // // 所有的注册函数处理逻辑上都相似,碰上同名的会覆盖,否则是添加。 // 且默认情况下都提供了一些可选项,只有在用户需要额外添加自己的内容时才需要调用注册函数。 -func LoadOptions[T comparable](configDir, filename string) (*Options, T, error) { +func Load[T comparable](configDir, filename string) (*server.Options, T, error) { var zero T if filename == "" { - return &Options{Config: &Config{Dir: configDir}}, zero, nil + return &server.Options{Config: config.Dir(nil, configDir)}, zero, nil } conf, err := loadConfigOf[T](configDir, filename) @@ -161,7 +173,7 @@ func LoadOptions[T comparable](configDir, filename string) (*Options, T, error) return nil, zero, web.NewStackError(err) } - o := &Options{ + o := &server.Options{ Config: conf.config, Location: conf.location, Cache: conf.cache, @@ -171,8 +183,7 @@ func LoadOptions[T comparable](configDir, filename string) (*Options, T, error) RoutersOptions: make([]web.RouterOption, 0, 5), IDGenerator: conf.idGenerator, RequestIDKey: conf.HTTP.RequestID, - Compressions: conf.compressors, - Mimetypes: conf.mimetypes, + Codec: conf.codec, ProblemTypePrefix: conf.ProblemTypePrefix, OnRender: conf.onRender, Init: make([]web.PluginFunc, 0, 5), @@ -187,78 +198,48 @@ func LoadOptions[T comparable](configDir, filename string) (*Options, T, error) func (conf *configOf[T]) SanitizeConfig() *web.FieldError { if conf.MemoryLimit > 0 { - conf.init = append(conf.init, func(*Options) { debug.SetMemoryLimit(conf.MemoryLimit) }) - } - - if err := conf.buildCache(); err != nil { - return err.AddFieldParent("cache") + conf.init = append(conf.init, func(*server.Options) { debug.SetMemoryLimit(conf.MemoryLimit) }) } - if conf.Logs == nil { - conf.Logs = &logsConfig{} - } - - if err := conf.Logs.build(); err != nil { - return err.AddFieldParent("logs") - } - conf.init = append(conf.init, func(o *Options) { - o.Init = append(o.Init, func(s web.Server) { s.OnClose(conf.Logs.cleanup...) }) - }) - if conf.Language != "" { tag, err := language.Parse(conf.Language) if err != nil { - return web.NewFieldError("language.", err) + return web.NewFieldError("language", err) } conf.languageTag = tag } - if err := conf.buildTimezone(); err != nil { - return err + if err := conf.buildCache(); err != nil { + return err.AddFieldParent("cache") } - if conf.HTTP == nil { - conf.HTTP = &httpConfig{} - } - if err := conf.HTTP.sanitize(); err != nil { - return err.AddFieldParent("http") - } - if conf.HTTP.init != nil { - conf.init = append(conf.init, conf.HTTP.init) + if err := conf.buildLogs(); err != nil { + return err } - if err := conf.sanitizeCompresses(); err != nil { - return err.AddFieldParent("compressions") + if err := conf.buildTimezone(); err != nil { + return err } - if err := conf.sanitizeMimetypes(); err != nil { + if err := conf.buildHTTP(); err != nil { return err } - if err := conf.sanitizeFileSerializers(); err != nil { - return err.AddFieldParent("fileSerializer") + if err := conf.buildCodec(); err != nil { + return err } - if conf.IDGenerator == "" { - conf.IDGenerator = "date" - } - if g, found := idGeneratorFactory.get(conf.IDGenerator); found { - f, srv := g() - conf.idGenerator = f - if srv != nil { - conf.init = append(conf.init, func(o *Options) { - o.Init = append(o.Init, func(s web.Server) { - s.Services().Add(locales.UniqueIdentityGenerator, srv) - }) - }) - } + if err := conf.buildConfig(); err != nil { + return err } + conf.buildIDGen() + if conf.OnRender != "" { if or, found := onRenderFactory.get(conf.OnRender); found { conf.onRender = or } else { - return web.NewFieldError("OnRender", locales.ErrNotFound()) + return web.NewFieldError("onRender", locales.ErrNotFound()) } } @@ -278,6 +259,23 @@ func (conf *configOf[T]) SanitizeConfig() *web.FieldError { return nil } +func (conf *configOf[T]) buildIDGen() { + if conf.IDGenerator == "" { + conf.IDGenerator = "date" + } + if g, found := idGeneratorFactory.get(conf.IDGenerator); found { + f, srv := g() + conf.idGenerator = f + if srv != nil { + conf.init = append(conf.init, func(o *server.Options) { + o.Init = append(o.Init, func(s web.Server) { + s.Services().Add(locales.UniqueIdentityGenerator, srv) + }) + }) + } + } +} + func (conf *configOf[T]) buildTimezone() *web.FieldError { if conf.Timezone == "" { return nil @@ -297,3 +295,23 @@ func CheckConfigSyntax[T comparable](configDir, filename string) error { _, err := loadConfigOf[T](configDir, filename) return err } + +// NewPrinter 根据参数构建一个本地化的打印对象 +// +// 语言由 [localeutil.DetectUserLanguageTag] 决定。 +// fsys 指定了加载本地化文件的文件系统,glob 则指定了加载的文件匹配规则; +// 对于文件的序列化方式则是根据后缀名从由 [RegisterFileSerializer] 注册的项中查找。 +func NewPrinter(glob string, fsys ...fs.FS) (*localeutil.Printer, error) { + tag, err := localeutil.DetectUserLanguageTag() + if err != nil { + log.Println(err) // 输出错误,但是不中断执行 + } + + b := catalog.NewBuilder(catalog.Fallback(tag)) + if err := locale.Load(buildSerializerFromFactory(), b, glob, fsys...); err != nil { + return nil, err + } + + p, _ := locale.NewPrinter(tag, b) + return p, nil +} diff --git a/server/config_test.go b/server/config/config_test.go similarity index 80% rename from server/config_test.go rename to server/config/config_test.go index a1ca0496..863eb79a 100644 --- a/server/config_test.go +++ b/server/config/config_test.go @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: MIT -package server +package config import ( "io/fs" @@ -11,6 +11,8 @@ import ( "github.com/issue9/assert/v4" "github.com/issue9/config" "golang.org/x/text/language" + + "github.com/issue9/web/locales" ) var _ config.Sanitizer = &configOf[empty]{} @@ -28,14 +30,14 @@ func (u *userData) SanitizeConfig() *config.FieldError { return nil } -func TestLoadOptions(t *testing.T) { +func TestLoad(t *testing.T) { a := assert.New(t, false) - s, data, err := LoadOptions[empty]("./testdata", "web.yaml") + s, data, err := Load[empty]("./testdata", "web.yaml") a.NotError(err).NotNil(s).Equal(data, empty{}). Length(s.Init, 3) // cache, idgen, logs - s, data, err = LoadOptions[empty]("./testdata/not-exists", "web.yaml") + s, data, err = Load[empty]("./testdata/not-exists", "web.yaml") a.ErrorIs(err, fs.ErrNotExist).Nil(s).Equal(data, empty{}) } @@ -66,3 +68,9 @@ func TestConfig_buildTimezone(t *testing.T) { err := conf.buildTimezone() a.NotNil(err).Equal(err.Field, "timezone") } + +func TestNewPrinter(t *testing.T) { + a := assert.New(t, false) + p, err := NewPrinter("*.yaml", locales.Locales...) + a.NotError(err).NotNil(p) +} diff --git a/server/config/file.go b/server/config/file.go new file mode 100644 index 00000000..b12fdcca --- /dev/null +++ b/server/config/file.go @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2018-2024 caixw +// +// SPDX-License-Identifier: MIT + +package config + +import ( + "strconv" + + "github.com/issue9/config" + + "github.com/issue9/web" +) + +type fileSerializer struct { + exts []string // 支持的扩展名 + marshal config.MarshalFunc + unmarshal config.UnmarshalFunc +} + +func (conf *configOf[T]) buildConfig() *web.FieldError { + ss := make(config.Serializer, len(fileSerializerFactory.items)) + for i, name := range conf.FileSerializers { + s, found := fileSerializerFactory.get(name) + if !found { + return web.NewFieldError("fileSerializers["+strconv.Itoa(i)+"]", web.NewLocaleError("not found serialization function for %s", name)) + } + ss.Add(s.marshal, s.unmarshal, s.exts...) + } + + c, err := config.BuildDir(ss, conf.dir) + if err != nil { + return web.NewFieldError("", err) // 应该是与目录相关的错误引起的 + } + + conf.config = c + return nil +} + +func loadConfigOf[T comparable](configDir, name string) (*configOf[T], error) { + c, err := config.BuildDir(buildSerializerFromFactory(), configDir) + if err != nil { + return nil, err + } + + conf := &configOf[T]{dir: configDir} + if err := c.Load(name, conf); err != nil { + return nil, err + } + return conf, nil +} + +func buildSerializerFromFactory() config.Serializer { + s := make(config.Serializer, len(fileSerializerFactory.items)) + for _, item := range fileSerializerFactory.items { + s.Add(item.marshal, item.unmarshal, item.exts...) + } + return s +} diff --git a/server/file_test.go b/server/config/file_test.go similarity index 87% rename from server/file_test.go rename to server/config/file_test.go index 01f5b7cb..6237c867 100644 --- a/server/file_test.go +++ b/server/config/file_test.go @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: MIT -package server +package config import ( "io/fs" @@ -12,8 +12,6 @@ import ( "github.com/issue9/assert/v4" "github.com/issue9/config" - - "github.com/issue9/web/locales" ) func TestLoadConfigOf(t *testing.T) { @@ -53,9 +51,3 @@ func TestLoadConfigOf(t *testing.T) { a.NotError(err).NotNil(customConf) a.Equal(customConf.User.ID, 1) } - -func TestNewPrinter(t *testing.T) { - a := assert.New(t, false) - p, err := NewPrinter("*.yaml", locales.Locales...) - a.NotError(err).NotNil(p) -} diff --git a/server/http.go b/server/config/http.go similarity index 93% rename from server/http.go rename to server/config/http.go index 1b55c452..655925a4 100644 --- a/server/http.go +++ b/server/config/http.go @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: MIT -package server +package config import ( "crypto/tls" @@ -12,12 +12,14 @@ import ( "os" "time" + "github.com/issue9/logs/v7" "github.com/issue9/mux/v8" "github.com/issue9/mux/v8/header" "golang.org/x/crypto/acme/autocert" "github.com/issue9/web" "github.com/issue9/web/locales" + "github.com/issue9/web/server" ) type ( @@ -78,7 +80,7 @@ type ( // NOTE: 这些设置对所有路径均有效,但会被 [web.Routers.New] 的参数修改。 Trace bool `yaml:"trace,omitempty" json:"trace,omitempty" xml:"trace,omitempty"` - init func(*Options) + init func(*server.Options) httpServer *http.Server } @@ -137,6 +139,20 @@ type ( } ) +func (conf *configOf[T]) buildHTTP() *web.FieldError { + if conf.HTTP == nil { + conf.HTTP = &httpConfig{} + } + if err := conf.HTTP.sanitize(conf.Logs.logs); err != nil { + return err.AddFieldParent("http") + } + if conf.HTTP.init != nil { + conf.init = append(conf.init, conf.HTTP.init) + } + + return nil +} + func exists(p string) bool { _, err := os.Stat(p) return err == nil || errors.Is(err, fs.ErrExist) @@ -154,7 +170,7 @@ func (cert *certificateConfig) sanitize() *web.FieldError { return nil } -func (h *httpConfig) sanitize() *web.FieldError { +func (h *httpConfig) sanitize(l *logs.Logs) *web.FieldError { if h.ReadTimeout < 0 { return web.NewFieldError("readTimeout", locales.ShouldGreatThan(0)) } @@ -183,7 +199,7 @@ func (h *httpConfig) sanitize() *web.FieldError { return err } - h.init = func(o *Options) { + h.init = func(o *server.Options) { if len(h.Headers) > 0 { o.Init = append(o.Init, func(s web.Server) { s.Routers().Use(web.MiddlewareFunc(func(next web.HandlerFunc) web.HandlerFunc { @@ -197,7 +213,7 @@ func (h *httpConfig) sanitize() *web.FieldError { }) } - h.buildRoutersOptions(o) + h.buildRoutersOptions(o, l) } h.buildHTTPServer() @@ -216,11 +232,11 @@ func (h *httpConfig) buildHTTPServer() { } } -func (h *httpConfig) buildRoutersOptions(o *Options) { +func (h *httpConfig) buildRoutersOptions(o *server.Options, l *logs.Logs) { opt := make([]web.RouterOption, 0, 3) if h.Recovery > 0 { - opt = append(opt, web.Recovery(h.Recovery, o.logs.ERROR())) + opt = append(opt, web.Recovery(h.Recovery, l.ERROR())) } if h.CORS != nil { diff --git a/server/http_test.go b/server/config/http_test.go similarity index 95% rename from server/http_test.go rename to server/config/http_test.go index 3e300401..80731ccd 100644 --- a/server/http_test.go +++ b/server/config/http_test.go @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: MIT -package server +package config import ( "encoding" @@ -12,6 +12,7 @@ import ( "time" "github.com/issue9/assert/v4" + "github.com/issue9/logs/v7" "gopkg.in/yaml.v3" ) @@ -30,20 +31,21 @@ func TestCertificate_sanitize(t *testing.T) { func TestHTTP_sanitize(t *testing.T) { a := assert.New(t, false) + l := logs.New(logs.NewNopHandler()) http := &httpConfig{} http.ReadTimeout = -1 - ferr := http.sanitize() + ferr := http.sanitize(l) a.Equal(ferr.Field, "readTimeout") http.ReadTimeout = 0 http.IdleTimeout = -1 - ferr = http.sanitize() + ferr = http.sanitize(l) a.Equal(ferr.Field, "idleTimeout") http.IdleTimeout = 0 http.ReadHeaderTimeout = -1 - ferr = http.sanitize() + ferr = http.sanitize(l) a.Equal(ferr.Field, "readHeaderTimeout") } diff --git a/server/logs.go b/server/config/logs.go similarity index 63% rename from server/logs.go rename to server/config/logs.go index 31cea7ab..8577a71a 100644 --- a/server/logs.go +++ b/server/config/logs.go @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: MIT -package server +package config import ( "errors" @@ -11,8 +11,6 @@ import ( "strconv" "strings" - "github.com/issue9/config" - "github.com/issue9/localeutil" "github.com/issue9/logs/v7" "github.com/issue9/logs/v7/writers" "github.com/issue9/logs/v7/writers/rotate" @@ -20,47 +18,9 @@ import ( "github.com/issue9/web" "github.com/issue9/web/locales" + "github.com/issue9/web/server" ) -// 日志的时间格式 -const ( - DateMilliLayout = logs.DateMilliLayout - DateMicroLayout = logs.DateMicroLayout - DateNanoLayout = logs.DateNanoLayout - - MilliLayout = logs.MilliLayout - MicroLayout = logs.MicroLayout - NanoLayout = logs.NanoLayout -) - -// Logs 初始化日志的选项 -type Logs struct { - // Handler 后端处理接口 - // - // 内置了以下几种方式: - // - [NewNopHandler] - // - [NewTermHandler] - // - [NewTextHandler] - // - [NewJSONHandler] - Handler logs.Handler - - // 是否带调用堆栈信息 - Location bool - - // 指定创建日志的时间格式,如果为空表示不需要输出时间。 - Created string - - // 允许的日志级别 - Levels []logs.Level - - // 对于 [Logger.Error] 输入 [xerrors.Formatter] 类型时, - // 是否输出调用堆栈信息。 - StackError bool - - // 是否接管标准库日志的输出 - Std bool -} - // LogsHandlerBuilder 构建 [logs.Handler] 的方法 type LogsHandlerBuilder = func(args []string) (logs.Handler, func() error, error) @@ -81,12 +41,15 @@ type logsConfig struct { // 是否接管标准库的日志 Std bool `xml:"std,attr,omitempty" json:"std,omitempty" yaml:"std,omitempty"` + // 是否显示错误日志的调用堆栈 + StackError bool `xml:"stackError,attr,omitempty" json:"stackError,omitempty" yaml:"stackError,omitempty"` + // 日志输出对象的配置 // // 为空表示 [NewNopHandler] 返回的对象。 Handlers []*logHandlerConfig `xml:"handlers>handler" json:"handlers" yaml:"handlers"` - logs *Logs + logs *logs.Logs cleanup []func() error } @@ -144,110 +107,53 @@ type logHandlerConfig struct { Args []string `xml:"arg,omitempty" yaml:"args,omitempty" json:"args,omitempty"` } -func (o *Options) buildLogs(p *localeutil.Printer) *web.FieldError { - if o.Logs == nil { - o.Logs = &Logs{} - } - - if o.Logs.Handler == nil { - o.Logs.Handler = NewNopHandler() +func (conf *configOf[T]) buildLogs() *web.FieldError { + if conf.Logs == nil { + conf.Logs = &logsConfig{} } - - for index, lv := range o.Logs.Levels { - if !logs.IsValidLevel(lv) { - field := "Logs.Levels[" + strconv.Itoa(index) + "]" - return config.NewFieldError(field, locales.InvalidValue) - } + if err := conf.Logs.build(); err != nil { + return err.AddFieldParent("logs") } + conf.init = append(conf.init, func(o *server.Options) { + o.Init = append(o.Init, func(s web.Server) { s.OnClose(conf.Logs.cleanup...) }) + }) - oo := make([]logs.Option, 0, 5) + return nil +} - oo = append(oo, logs.WithLocale(p)) +func (conf *logsConfig) build() *web.FieldError { + defer func() { + if conf.logs == nil { + conf.logs = logs.New(logs.NewNopHandler()) + } + }() - if o.Logs.Location { - oo = append(oo, logs.WithLocation(true)) - } - if o.Logs.Created != "" { - oo = append(oo, logs.WithCreated(o.Logs.Created)) - } - if o.Logs.StackError { - oo = append(oo, logs.WithDetail(true)) + if len(conf.Levels) == 0 { // 确保 buildHandler() 从 conf.Levels 继承的数据不是空的 + conf.Levels = logs.AllLevels() } - if o.Logs.Std { - oo = append(oo, logs.WithStd()) + o := make([]logs.Option, 0, 5) + if conf.Location { + o = append(o, logs.WithLocation(true)) } - - if len(o.Logs.Levels) > 0 { - oo = append(oo, logs.WithLevels(o.Logs.Levels...)) + if conf.Created != "" { + o = append(o, logs.WithCreated(conf.Created)) } - - o.logs = logs.New(o.Logs.Handler, oo...) - - return nil -} - -// AllLevels 返回所有的日志类型 -func AllLevels() []logs.Level { return logs.AllLevels() } - -// NewTextHandler 声明文本类型的日志输出通道 -func NewTextHandler(w ...io.Writer) logs.Handler { return logs.NewTextHandler(w...) } - -// NewJSONHandler 声明 JSON 类型的日志输出通道 -func NewJSONHandler(w ...io.Writer) logs.Handler { return logs.NewJSONHandler(w...) } - -// NewTermHandler 带颜色的终端输出通道 -// -// 参数说明参考 [logs.NewTermHandler] -func NewTermHandler(w io.Writer, colors map[logs.Level]colors.Color) logs.Handler { - return logs.NewTermHandler(w, colors) -} - -func NewNopHandler() logs.Handler { return logs.NewNopHandler() } - -// NewDispatchHandler 按不同的 [Level] 派发到不同的 [Handler] 对象 -func NewDispatchHandler(d map[logs.Level]logs.Handler) logs.Handler { - return logs.NewDispatchHandler(d) -} - -// MergeHandler 合并多个 [Handler] 对象 -func MergeHandler(w ...logs.Handler) logs.Handler { return logs.MergeHandler(w...) } - -// NewRotateFile 按大小分割的文件日志 -// -// 参数说明参考 [rotate.New] -func NewRotateFile(format, dir string, size int64) (io.WriteCloser, error) { - return rotate.New(format, dir, size) -} - -// NewSMTP 将日志内容发送至指定邮箱 -// -// 参数说明参考 [writers.NewSMTP] -func NewSMTP(username, password, subject, host string, sendTo []string) io.Writer { - return writers.NewSMTP(username, password, subject, host, sendTo) -} - -func (conf *logsConfig) build() *web.FieldError { - if conf.logs == nil { - conf.logs = &Logs{} + if conf.StackError { + o = append(o, logs.WithDetail(true)) } - - if len(conf.Levels) == 0 { // 确保 buildHandler() 从 conf.Levels 继承的数据不是空的 - conf.Levels = AllLevels() + if conf.Std { + o = append(o, logs.WithStd()) + } + if len(conf.Levels) > 0 { + o = append(o, logs.WithLevels(conf.Levels...)) } - w, c, err := conf.buildHandler() + h, c, err := conf.buildHandler() if err != nil { return err } - - conf.logs = &Logs{ - Handler: w, - Created: conf.Created, - Location: conf.Location, - Levels: conf.Levels, - Std: conf.Std, - } + conf.logs = logs.New(h, o...) conf.cleanup = c return nil @@ -309,10 +215,10 @@ func (conf *logsConfig) buildHandler() (logs.Handler, []func() error, *web.Field } d := make(map[logs.Level]logs.Handler, len(m)) - for _, l := range AllLevels() { + for _, l := range logs.AllLevels() { switch ws := m[l]; { case ws == nil: - d[l] = NewNopHandler() + d[l] = logs.NewNopHandler() case len(ws) == 1: d[l] = ws[0] default: @@ -329,25 +235,25 @@ func newFileLogsHandler(args []string) (logs.Handler, func() error, error) { return nil, nil, err } - w, err := NewRotateFile(args[1], args[0], size) + w, err := rotate.New(args[1], args[0], size) if err != nil { return nil, nil, err } if len(args) < 4 || args[3] == "text" { - return NewTextHandler(w), w.Close, nil + return logs.NewTextHandler(w), w.Close, nil } - return NewJSONHandler(w), w.Close, nil + return logs.NewJSONHandler(w), w.Close, nil } func newSMTPLogsHandler(args []string) (logs.Handler, func() error, error) { sendTo := strings.Split(args[4], ",") - w := NewSMTP(args[0], args[1], args[2], args[3], sendTo) + w := writers.NewSMTP(args[0], args[1], args[2], args[3], sendTo) if len(args) < 6 || args[6] == "text" { - return NewTextHandler(w), nil, nil + return logs.NewTextHandler(w), nil, nil } - return NewJSONHandler(w), nil, nil + return logs.NewJSONHandler(w), nil, nil } var colorMap = map[string]colors.Color{ @@ -405,5 +311,5 @@ func newTermLogsHandler(args []string) (logs.Handler, func() error, error) { cs[lv] = c } - return NewTermHandler(w, cs), nil, nil + return logs.NewTermHandler(w, cs), nil, nil } diff --git a/server/logs_test.go b/server/config/logs_test.go similarity index 82% rename from server/logs_test.go rename to server/config/logs_test.go index f665ba71..9b97971a 100644 --- a/server/logs_test.go +++ b/server/config/logs_test.go @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: MIT -package server +package config import ( "testing" @@ -19,15 +19,15 @@ func TestLogsConfig_build(t *testing.T) { conf := &logsConfig{} err := conf.build() a.NotError(err).NotNil(conf.logs).Length(conf.cleanup, 0). - Equal(conf.logs.Levels, AllLevels()). - Empty(conf.logs.Created) + Equal(conf.Levels, logs.AllLevels()). + Empty(conf.Created) conf = &logsConfig{Levels: []logs.Level{logs.LevelWarn, logs.LevelError}, Created: logs.NanoLayout} err = conf.build() a.NotError(err).NotNil(conf.logs).Length(conf.cleanup, 0). - Equal(conf.logs.Levels, []logs.Level{logs.LevelWarn, logs.LevelError}). - Equal(conf.logs.Created, logs.NanoLayout). - False(conf.logs.Location) + Equal(conf.Levels, []logs.Level{logs.LevelWarn, logs.LevelError}). + Equal(conf.Created, logs.NanoLayout). + False(conf.Location) } func TestLogsConfig_buildHandler(t *testing.T) { @@ -36,7 +36,7 @@ func TestLogsConfig_buildHandler(t *testing.T) { // len(Handlers) == 0 conf := &logsConfig{} h, c, err := conf.buildHandler() - a.NotError(err).Equal(h, NewNopHandler()).Nil(c) + a.NotError(err).Equal(h, logs.NewNopHandler()).Nil(c) // len(Handlers) == 1 conf = &logsConfig{ @@ -46,7 +46,7 @@ func TestLogsConfig_buildHandler(t *testing.T) { }, } h, c, err = conf.buildHandler() - a.NotError(err).NotNil(h).NotEqual(h, NewNopHandler()).NotNil(c) + a.NotError(err).NotNil(h).NotEqual(h, logs.NewNopHandler()).NotNil(c) // len(Handlers) > 1 conf = &logsConfig{ @@ -57,7 +57,7 @@ func TestLogsConfig_buildHandler(t *testing.T) { }, } h, c, err = conf.buildHandler() - a.NotError(err).NotNil(h).NotEqual(h, NewNopHandler()).NotNil(c) + a.NotError(err).NotNil(h).NotEqual(h, logs.NewNopHandler()).NotNil(c) } func TestNewTermHandler(t *testing.T) { diff --git a/server/micro.go b/server/config/micro.go similarity index 93% rename from server/micro.go rename to server/config/micro.go index ca513261..9fb1637a 100644 --- a/server/micro.go +++ b/server/config/micro.go @@ -2,12 +2,14 @@ // // SPDX-License-Identifier: MIT -package server +package config import ( "strconv" "strings" + "github.com/issue9/mux/v8/group" + "github.com/issue9/web" "github.com/issue9/web/locales" "github.com/issue9/web/server/registry" @@ -24,11 +26,6 @@ type ( RouterMatcherBuilder = func(...string) web.RouterMatcher - // Mapper 微服务名称与路由的匹配关系 - // - // 在网关中由此列表确定相应的路由由哪个微服务进行代码。 - Mapper map[string]web.RouterMatcher - // registryConfig 注册服务中心的配置项 registryConfig struct { // 配置的保存类型 @@ -95,7 +92,7 @@ func (conf *configOf[T]) buildMicro(c web.Cache) *web.FieldError { } if len(conf.Mappers) > 0 { - conf.mapper = Mapper{} + conf.mapper = make(map[string]group.Matcher, len(conf.Mappers)) for i, m := range conf.Mappers { mm, found := routerMatcherFactory.get(m.Matcher) if !found { @@ -109,7 +106,7 @@ func (conf *configOf[T]) buildMicro(c web.Cache) *web.FieldError { } func (r *registryConfig) build(c web.Cache) *web.FieldError { - t, found := typeFactory.get(r.Type) + t, found := registryTypeFactory.get(r.Type) if !found { return web.NewFieldError("type", locales.ErrNotFound()) } diff --git a/server/micro_test.go b/server/config/micro_test.go similarity index 99% rename from server/micro_test.go rename to server/config/micro_test.go index 2e7f619d..316221a6 100644 --- a/server/micro_test.go +++ b/server/config/micro_test.go @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: MIT -package server +package config import ( "testing" diff --git a/server/register.go b/server/config/register.go similarity index 79% rename from server/register.go rename to server/config/register.go index 99cbc4a9..7d87fd20 100644 --- a/server/register.go +++ b/server/config/register.go @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: MIT -package server +package config import ( "compress/flate" @@ -17,8 +17,12 @@ import ( "github.com/andybalholm/brotli" "github.com/issue9/cache" + "github.com/issue9/cache/caches/memcache" + "github.com/issue9/cache/caches/memory" + "github.com/issue9/cache/caches/redis" "github.com/issue9/config" "github.com/issue9/mux/v8/group" + "github.com/issue9/unique/v2" "gopkg.in/yaml.v3" "github.com/issue9/web" @@ -30,6 +34,7 @@ import ( "github.com/issue9/web/mimetype/json" "github.com/issue9/web/mimetype/nop" "github.com/issue9/web/mimetype/xml" + "github.com/issue9/web/server" "github.com/issue9/web/server/registry" ) @@ -61,28 +66,23 @@ func (r *register[T]) get(name string) (T, bool) { // 以下为所有 register 的实例化类型及关联的操作 var ( - logHandlersFactory = newRegister[LogsHandlerBuilder]() - cacheFactory = newRegister[CacheBuilder]() - compressorFactory = newRegister[compressor.Compressor]() - idGeneratorFactory = newRegister[IDGeneratorBuilder]() - mimetypesFactory = newRegister[mimetypeItem]() - filesFactory = newRegister[*FileSerializer]() - routerMatcherFactory = newRegister[RouterMatcherBuilder]() - onRenderFactory = newRegister[func(int, any) (int, any)]() - - strategyFactory = newRegister[StrategyBuilder]() - typeFactory = newRegister[RegistryTypeBuilder]() + logHandlersFactory = newRegister[LogsHandlerBuilder]() + cacheFactory = newRegister[CacheBuilder]() + compressorFactory = newRegister[compressor.Compressor]() + idGeneratorFactory = newRegister[IDGeneratorBuilder]() + mimetypesFactory = newRegister[mimetype]() + fileSerializerFactory = newRegister[fileSerializer]() + routerMatcherFactory = newRegister[RouterMatcherBuilder]() + onRenderFactory = newRegister[func(int, any) (int, any)]() + + strategyFactory = newRegister[StrategyBuilder]() + registryTypeFactory = newRegister[RegistryTypeBuilder]() ) -type mimetypeItem struct { - marshal web.MarshalFunc - unmarshal web.UnmarshalFunc -} - // IDGeneratorBuilder 构建生成唯一 ID 的方法 // // f 表示生成唯一 ID 的方法;s 为 f 依赖的服务,可以为空; -type IDGeneratorBuilder = func() (f IDGenerator, s web.Service) +type IDGeneratorBuilder = func() (f func() string, s web.Service) // RegisterLogsHandler 注册日志的 [LogsWriterBuilder] // @@ -110,7 +110,7 @@ func RegisterIDGenerator(id string, b IDGeneratorBuilder) { idGeneratorFactory.r // // name 为名称,这将在配置文件中被引用,如果存在同名,则会覆盖。 func RegisterMimetype(m web.MarshalFunc, u web.UnmarshalFunc, name string) { - mimetypesFactory.register(mimetypeItem{marshal: m, unmarshal: u}, name) + mimetypesFactory.register(mimetype{marshal: m, unmarshal: u}, name) } // RegisterFileSerializer 注册用于文件序列化的方法 @@ -119,18 +119,18 @@ func RegisterMimetype(m web.MarshalFunc, u web.UnmarshalFunc, name string) { // ext 为文件的扩展名; func RegisterFileSerializer(name string, m config.MarshalFunc, u config.UnmarshalFunc, ext ...string) { for _, e := range ext { - for k, s := range filesFactory.items { - if slices.Index(s.Exts, e) >= 0 { + for k, s := range fileSerializerFactory.items { + if slices.Index(s.exts, e) >= 0 { panic(fmt.Sprintf("扩展名 %s 已经注册到 %s", e, k)) } } } - filesFactory.register(&FileSerializer{Marshal: m, Unmarshal: u, Exts: ext}, name) + fileSerializerFactory.register(fileSerializer{marshal: m, unmarshal: u, exts: ext}, name) } func RegisterStrategy(f StrategyBuilder, name string) { strategyFactory.register(f, name) } -func RegisterRegistryType(f RegistryTypeBuilder, name string) { typeFactory.register(f, name) } +func RegisterRegistryType(f RegistryTypeBuilder, name string) { registryTypeFactory.register(f, name) } func RegisterRouterMatcher(f RouterMatcherBuilder, name string) { routerMatcherFactory.register(f, name) @@ -153,16 +153,20 @@ func init() { return nil, nil, err } - drv, job := NewMemory() - return drv, &Job{Ticker: d, Job: job}, nil + drv, job := memory.New() + j := func(now time.Time) error { + job(now) + return nil + } + return drv, &Job{Ticker: d, Job: j}, nil }, "memory") RegisterCache(func(dsn string) (cache.Driver, *Job, error) { - return NewMemcache(strings.Split(dsn, ";")...), nil, nil + return memcache.New(strings.Split(dsn, ";")...), nil, nil }, "memcached", "memcache") RegisterCache(func(dsn string) (cache.Driver, *Job, error) { - drv, err := NewRedisFromURL(dsn) + drv, err := redis.NewFromURL(dsn) if err != nil { return nil, nil, err } @@ -190,9 +194,18 @@ func init() { // RegisterIDGenerator - RegisterIDGenerator("date", func() (IDGenerator, web.Service) { return DateID(100) }) - RegisterIDGenerator("string", func() (IDGenerator, web.Service) { return StringID(100) }) - RegisterIDGenerator("number", func() (IDGenerator, web.Service) { return NumberID(100) }) + RegisterIDGenerator("date", func() (func() string, web.Service) { + u := unique.NewNumber(100) + return u.String, u + }) + RegisterIDGenerator("string", func() (func() string, web.Service) { + u := unique.NewString(100) + return u.String, u + }) + RegisterIDGenerator("number", func() (func() string, web.Service) { + u := unique.NewDate(100) + return u.String, u + }) // RegisterMimetype @@ -240,5 +253,5 @@ func init() { // OnRender - RegisterOnRender(Render200, "render200") + RegisterOnRender(server.Render200, "render200") } diff --git a/server/register_test.go b/server/config/register_test.go similarity index 80% rename from server/register_test.go rename to server/config/register_test.go index 5a8b7b5c..79d98cf6 100644 --- a/server/register_test.go +++ b/server/config/register_test.go @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: MIT -package server +package config import ( "encoding/json" @@ -19,6 +19,6 @@ func TestRegisterFileSerializer(t *testing.T) { }, "扩展名 .json 已经注册到 json") RegisterFileSerializer("new", json.Marshal, json.Unmarshal, ".js") - v, f := filesFactory.get("new") - a.True(f).NotNil(v).Equal(v.Exts, []string{".js"}) + v, f := fileSerializerFactory.get("new") + a.True(f).NotNil(v).Equal(v.exts, []string{".js"}) } diff --git a/server/testdata/cert.pem b/server/config/testdata/cert.pem similarity index 100% rename from server/testdata/cert.pem rename to server/config/testdata/cert.pem diff --git a/server/testdata/invalid-web.xml b/server/config/testdata/invalid-web.xml similarity index 100% rename from server/testdata/invalid-web.xml rename to server/config/testdata/invalid-web.xml diff --git a/server/testdata/key.pem b/server/config/testdata/key.pem similarity index 100% rename from server/testdata/key.pem rename to server/config/testdata/key.pem diff --git a/server/testdata/req.cnf b/server/config/testdata/req.cnf similarity index 100% rename from server/testdata/req.cnf rename to server/config/testdata/req.cnf diff --git a/server/testdata/ssh-keygen.sh b/server/config/testdata/ssh-keygen.sh similarity index 77% rename from server/testdata/ssh-keygen.sh rename to server/config/testdata/ssh-keygen.sh index 4e4995d5..03fbc0a5 100755 --- a/server/testdata/ssh-keygen.sh +++ b/server/config/testdata/ssh-keygen.sh @@ -1,5 +1,7 @@ #!/bin/bash +# SPDX-FileCopyrightText: 2018-2024 caixw +# # SPDX-License-Identifier: MIT - + openssl req -newkey rsa:2048 -x509 -nodes -keyout key.pem -new -out cert.pem -config req.cnf -sha256 -days 3650 diff --git a/server/testdata/user.xml b/server/config/testdata/user.xml similarity index 100% rename from server/testdata/user.xml rename to server/config/testdata/user.xml diff --git a/server/testdata/web.json b/server/config/testdata/web.json similarity index 100% rename from server/testdata/web.json rename to server/config/testdata/web.json diff --git a/server/testdata/web.xml b/server/config/testdata/web.xml similarity index 100% rename from server/testdata/web.xml rename to server/config/testdata/web.xml diff --git a/server/testdata/web.yaml b/server/config/testdata/web.yaml similarity index 100% rename from server/testdata/web.yaml rename to server/config/testdata/web.yaml diff --git a/server/file.go b/server/file.go deleted file mode 100644 index 0f6c6c6e..00000000 --- a/server/file.go +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-FileCopyrightText: 2018-2024 caixw -// -// SPDX-License-Identifier: MIT - -package server - -import ( - "io/fs" - "log" - "strconv" - - "github.com/issue9/config" - "github.com/issue9/localeutil" - "golang.org/x/text/message/catalog" - - "github.com/issue9/web" - "github.com/issue9/web/internal/locale" -) - -func (conf *configOf[T]) sanitizeFileSerializers() *web.FieldError { - for i, name := range conf.FileSerializers { - s, found := filesFactory.get(name) - if !found { - return web.NewFieldError("["+strconv.Itoa(i)+"]", web.NewLocaleError("not found serialization function for %s", name)) - } - conf.config.Serializers = append(conf.config.Serializers, s) - } - return nil -} - -func loadConfigOf[T comparable](configDir, name string) (*configOf[T], error) { - c, err := config.BuildDir(buildSerializerFromFactory(), configDir) - if err != nil { - return nil, err - } - - conf := &configOf[T]{config: &Config{Dir: configDir}} - if err := c.Load(name, conf); err != nil { - return nil, err - } - return conf, nil -} - -func buildSerializerFromFactory() config.Serializer { - s := make(config.Serializer, len(filesFactory.items)) - for _, item := range filesFactory.items { - s.Add(item.Marshal, item.Unmarshal, item.Exts...) - } - return s -} - -// NewPrinter 根据参数构建一个本地化的打印对象 -// -// 语言由 [localeutil.DetectUserLanguageTag] 决定。 -// fsys 指定了加载本地化文件的文件系统,glob 则指定了加载的文件匹配规则; -// 对于文件的序列化方式则是根据后缀名从由 [RegisterFileSerializer] 注册的项中查找。 -func NewPrinter(glob string, fsys ...fs.FS) (*localeutil.Printer, error) { - tag, err := localeutil.DetectUserLanguageTag() - if err != nil { - log.Println(err) // 输出错误,但是不中断执行 - } - - b := catalog.NewBuilder(catalog.Fallback(tag)) - if err := locale.Load(buildSerializerFromFactory(), b, glob, fsys...); err != nil { - return nil, err - } - - p, _ := locale.NewPrinter(tag, b) - return p, nil -} diff --git a/server/options.go b/server/options.go index b6b8bccb..64700e6a 100644 --- a/server/options.go +++ b/server/options.go @@ -11,9 +11,7 @@ import ( "time" "github.com/issue9/cache" - "github.com/issue9/cache/caches/memcache" "github.com/issue9/cache/caches/memory" - "github.com/issue9/cache/caches/redis" "github.com/issue9/config" "github.com/issue9/localeutil" "github.com/issue9/logs/v7" @@ -26,6 +24,7 @@ import ( "github.com/issue9/web" "github.com/issue9/web/internal/locale" "github.com/issue9/web/locales" + xj "github.com/issue9/web/mimetype/json" "github.com/issue9/web/selector" "github.com/issue9/web/server/registry" ) @@ -42,13 +41,12 @@ type ( // Options [web.Server] 的初始化参数 // // 这些参数都有默认值,且无法在 [web.Server] 初始化之后进行更改。 - // - // 初始化方式,可以直接采用 &Options{...} 的方式,表示所有项都采用默认值。 - // 也可以采用 [LoadOptions] 从配置文件中加载相应在的数据进行初始化。 Options struct { - // 项目的配置项 - Config *Config - config *config.Config + // 项目的配置文件管理 + // + // 如果为空,则采用 [DefaultConfigDir] 作为配置文件的目录, + // 同时加载 YAML、XML 和 JSON 三种文件类型的序列化方法。 + Config *config.Config // 服务器的时区 // @@ -57,22 +55,17 @@ type ( // 缓存系统 // - // 内置了以下几种驱动: - // - [NewMemory] - // - [NewMemcache] - // - [NewRedisFromURL] - // 如果为空,采用 [NewMemory] 作为默认值。 + // 如果为空,采用 [memory.New] 作为默认值。 Cache cache.Driver - // 日志的相关设置 + // 日志系统 // // 如果此值为空,表示不会输出任何信息。 - Logs *Logs - logs *logs.Logs + // + // 会调用 [logs.Logs.SetLocale] 设置为 [Language] 的值。 + Logs *logs.Logs // http.Server 实例的值 - // - // 可以为零值。 HTTPServer *http.Server // 生成唯一字符串的方法 @@ -82,11 +75,9 @@ type ( // 如果为空,将采用 [unique.NewString] 作为生成方法。 // // NOTE: 该值的修改,可能造成项目中的唯一 ID 不再唯一。 - IDGenerator IDGenerator + IDGenerator func() string // 路由选项 - // - // 可以为空。 RoutersOptions []web.RouterOption // 指定获取 x-request-id 内容的报头名 @@ -94,17 +85,10 @@ type ( // 如果为空,则采用 [header.XRequestID] 作为默认值 RequestIDKey string - // 可用的压缩类型 + // 编码方式 // - // 默认为空。表示不需要该功能。 - Compressions []*Compression - - // 指定可用的 mimetype - // - // 默认采用 [JSONMimetypes]。 - Mimetypes []*Mimetype - - codec *web.Codec // 由 Compressions 和 Mimetypes 形成 + // 如果为空,则仅支持 JSON 编码,不支持压缩方式。 + Codec *web.Codec // 默认的语言标签 // @@ -134,6 +118,8 @@ type ( // OnRender 可实现对渲染结果的调整 // + // 默认为空。 + // // NOTE: 该值的修改,可能造成所有接口返回数据结构的变化。 OnRender func(status int, body any) (int, any) @@ -157,68 +143,27 @@ type ( // Mapper 作为微服务网关时的 URL 映射关系 // // NOTE: 仅在 [NewGateway] 中才会有效果。 - Mapper Mapper - } - - // Config 项目配置文件的配置 - Config struct { - // Dir 项目配置目录 - // - // 如果涉及到需要读取配置文件的,可以指定此对象,之后可通过此对象统一处理各类配置文件。 - // 如果为空,则会采用 [DefaultConfigDir]。 - Dir string - - // Serializers 支持的序列化方法列表 - // - // 如果为空,则会默认支持 yaml、json 两种方式; - Serializers []*FileSerializer + Mapper map[string]web.RouterMatcher } - - // FileSerializer 对于文件序列化的配置 - FileSerializer struct { - // Exts 支持的扩展名 - Exts []string - - // Marshal 序列化方法 - Marshal config.MarshalFunc - - // Unmarshal 反序列化方法 - Unmarshal config.UnmarshalFunc - } - - // IDGenerator 生成唯一 ID 的函数 - IDGenerator = func() string ) -func (c *Config) asConfig() (*config.Config, error) { - s := make(config.Serializer, len(c.Serializers)) - for _, ser := range c.Serializers { - s.Add(ser.Marshal, ser.Unmarshal, ser.Exts...) - } - - return config.BuildDir(s, c.Dir) -} - -func sanitizeOptions(o *Options, t int) (*Options, *config.FieldError) { +func sanitizeOptions(o *Options, t int) (*Options, *web.FieldError) { if o == nil { o = &Options{} } if o.Config == nil { - o.Config = &Config{ - Dir: DefaultConfigDir, - Serializers: []*FileSerializer{ - {Exts: []string{".yaml", ".yml"}, Marshal: yaml.Marshal, Unmarshal: yaml.Unmarshal}, - {Exts: []string{".json"}, Marshal: json.Marshal, Unmarshal: json.Unmarshal}, - {Exts: []string{".xml"}, Marshal: xml.Marshal, Unmarshal: xml.Unmarshal}, - }, + s := make(config.Serializer, 4) + s.Add(json.Marshal, json.Unmarshal, ".json"). + Add(yaml.Marshal, yaml.Unmarshal, ".yaml", ".yml"). + Add(xml.Marshal, xml.Unmarshal, ".xml") + + c, err := config.BuildDir(s, DefaultConfigDir) + if err != nil { + return nil, web.NewFieldError("Config", err) } + o.Config = c } - cfg, err := o.Config.asConfig() - if err != nil { - return nil, config.NewFieldError("Config", err) - } - o.config = cfg if o.Location == nil { o.Location = time.Local @@ -237,17 +182,20 @@ func sanitizeOptions(o *Options, t int) (*Options, *config.FieldError) { } if o.Cache == nil { - c, job := NewMemory() + c, job := memory.New() o.Cache = c o.Init = append(o.Init, func(s web.Server) { - s.Services().AddTicker(locales.RecycleLocalCache, job, time.Minute, false, false) + s.Services().AddTicker(locales.RecycleLocalCache, func(now time.Time) error { + job(now) + return nil + }, time.Minute, false, false) }) } if o.Language == language.Und { tag, err := localeutil.DetectUserLanguageTag() if err != nil { - return nil, config.NewFieldError("Language", err) + return nil, web.NewFieldError("Language", err) } o.Language = tag } @@ -256,21 +204,20 @@ func sanitizeOptions(o *Options, t int) (*Options, *config.FieldError) { o.Catalog = catalog.NewBuilder(catalog.Fallback(o.Language)) } - o.locale = locale.New(o.Language, o.config, o.Catalog) + o.locale = locale.New(o.Language, o.Config, o.Catalog) - if err := o.buildLogs(o.locale.Printer()); err != nil { - return nil, err + if o.Logs == nil { + o.Logs = logs.New(logs.NewNopHandler()) } + o.Logs.SetLocale(o.locale.Printer()) if o.RequestIDKey == "" { o.RequestIDKey = header.XRequestID } - c, fe := buildCodec(o.Mimetypes, o.Compressions) - if fe != nil { - return nil, fe + if o.Codec == nil { + o.Codec = web.NewCodec().AddMimetype(xj.Mimetype, xj.Marshal, xj.Unmarshal, xj.ProblemMimetype) } - o.codec = c switch t { case typeHTTP: // 不需要处理任何数据 @@ -300,64 +247,26 @@ func sanitizeOptions(o *Options, t int) (*Options, *config.FieldError) { return o, nil } -// NewMemory 声明基于内在的缓存对象 -func NewMemory() (cache.Driver, web.JobFunc) { - d, job := memory.New() - return d, func(now time.Time) error { - job(now) - return nil - } -} - -// NewRedisFromURL 声明基于 redis 的缓存对象 -// -// 参数说明可参考 [redis.NewFromURL]。 -func NewRedisFromURL(url string) (cache.Driver, error) { return redis.NewFromURL(url) } - -// NewMemcache 声明基于 memcache 的缓存对象 -// -// 参数说明可参考 [memcache.New]。 -func NewMemcache(addr ...string) cache.Driver { return memcache.New(addr...) } - func (o *Options) internalServer(name, version string, s web.Server) *web.InternalServer { - return web.InternalNewServer(s, name, version, o.Location, o.logs, o.IDGenerator, o.locale, o.Cache, o.codec, o.RequestIDKey, o.ProblemTypePrefix, o.OnRender, o.RoutersOptions...) -} - -// NumberID 构建数字形式的唯一 ID -// -// NOTE: 基于时间戳,不能保证多实例模式下也具有唯一性。 -func NumberID(buffSize int) (IDGenerator, web.Service) { - u := unique.NewNumber(buffSize) - return u.String, u -} - -// StringID 构建包含任意字符的唯一 ID -// -// NOTE: 基于时间戳,不能保证多实例模式下也具有唯一性。 -func StringID(buffSize int) (IDGenerator, web.Service) { - u := unique.NewString(buffSize) - return u.String, u -} - -// DateID 构建日期格式的唯一 ID -// -// NOTE: 基于时间戳,不能保证多实例模式下也具有唯一性。 -func DateID(buffSize int) (IDGenerator, web.Service) { - u := unique.NewDate(buffSize) - return u.String, u + return web.InternalNewServer(s, name, version, + o.Location, o.Logs, o.IDGenerator, o.locale, + o.Cache, o.Codec, o.RequestIDKey, o.ProblemTypePrefix, + o.OnRender, o.RoutersOptions...) } // Render200 统一 API 的返回格式 // -// 状态码统一为 200;返回对象统一为 [Render200Response]; +// 适用 [Options.OnRender]。 +// +// 返回值中,状态码统一为 200。返回对象统一为 [RenderResponse]。 func Render200(status int, body any) (int, any) { - return http.StatusOK, &Render200Response{OK: !web.IsProblem(status), Status: status, Body: body} + return http.StatusOK, &RenderResponse{OK: !web.IsProblem(status), Status: status, Body: body} } -// Render200Response API 统一的返回格式 -type Render200Response struct { - XMLName struct{} `json:"-" yaml:"-" xml:"body"` - OK bool `json:"ok" yaml:"ok" xml:"ok,attr"` - Status int `json:"status" yaml:"status" xml:"status,attr"` - Body any `json:"body" yaml:"body" xml:"body"` +// RenderResponse API 统一的返回格式 +type RenderResponse struct { + XMLName struct{} `json:"-" yaml:"-" xml:"body" cbor:"-"` + OK bool `json:"ok" yaml:"ok" xml:"ok,attr" cbor:"ok"` // 是否是错误代码 + Status int `json:"status" yaml:"status" xml:"status,attr" cbor:"status"` // 原始的状态码 + Body any `json:"body" yaml:"body" xml:"body" cbor:"body"` } diff --git a/server/options_test.go b/server/options_test.go index b988be02..d84d3361 100644 --- a/server/options_test.go +++ b/server/options_test.go @@ -18,9 +18,11 @@ func TestSanitizeOptions(t *testing.T) { o, err := sanitizeOptions(nil, typeHTTP) a.NotError(err).NotNil(o). Equal(o.Location, time.Local). - NotNil(o.logs). + NotNil(o.Logs). NotNil(o.IDGenerator). - NotNil(o.config). - Equal(o.Config.Dir, DefaultConfigDir). - Equal(o.RequestIDKey, header.XRequestID) + NotNil(o.Config). + Equal(o.RequestIDKey, header.XRequestID). + NotNil(o.locale). + NotNil(o.Codec). + NotZero(len(o.Init)) } diff --git a/server/registry/registry_test.go b/server/registry/registry_test.go index 19793e59..118ce70b 100644 --- a/server/registry/registry_test.go +++ b/server/registry/registry_test.go @@ -9,6 +9,7 @@ import ( "os" "github.com/issue9/assert/v4" + "github.com/issue9/logs/v7" "github.com/issue9/web" "github.com/issue9/web/server" @@ -16,12 +17,7 @@ import ( func newTestServer(a *assert.Assertion) web.Server { srv, err := server.New("test", "1.0.0", &server.Options{ - Logs: &server.Logs{ - Handler: server.NewTermHandler(os.Stderr, nil), - Created: server.NanoLayout, - Location: true, - Levels: server.AllLevels(), - }, + Logs: logs.New(logs.NewTermHandler(os.Stderr, nil), logs.WithLevels(logs.AllLevels()...), logs.WithLocation(true), logs.WithCreated(logs.NanoLayout)), HTTPServer: &http.Server{Addr: ":8080"}, }) diff --git a/server/server.go b/server/server.go index 2b09a9d1..77bd8868 100644 --- a/server/server.go +++ b/server/server.go @@ -2,8 +2,6 @@ // // SPDX-License-Identifier: MIT -//go:generate web htmldoc -lang=zh-CN -dir=./ -o=./CONFIG.html -object=configOf - // Package server 提供与服务端实现相关的功能 // // 目前实现了三种类型的服务端: diff --git a/server/server_test.go b/server/server_test.go index 8fe668d8..95b2bf22 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -15,6 +15,7 @@ import ( "github.com/issue9/assert/v4" "github.com/issue9/cache" "github.com/issue9/cache/caches/memory" + "github.com/issue9/logs/v7" "github.com/issue9/mux/v8/group" "github.com/issue9/mux/v8/header" "golang.org/x/text/language" @@ -55,8 +56,8 @@ func TestNew(t *testing.T) { d, ok := srv.Cache().(cache.Driver) a.True(ok). NotNil(d). - NotNil(d.Driver()) - a.True(srv.CanCompress()) + NotNil(d.Driver()). + True(srv.CanCompress()) srv.SetCompress(false) a.False(srv.CanCompress()) } @@ -66,21 +67,12 @@ func newOptions(o *Options) *Options { o = &Options{HTTPServer: &http.Server{Addr: ":8080"}, Language: language.English} // 指定不存在的语言 } if o.Logs == nil { // 默认重定向到 os.Stderr - o.Logs = &Logs{ - Handler: NewTermHandler(os.Stderr, nil), - Location: true, - Created: NanoLayout, - Levels: AllLevels(), - } - } - if o.Compressions == nil { - o.Compressions = DefaultCompressions() + o.Logs = logs.New(logs.NewTermHandler(os.Stderr, nil), logs.WithLocation(true), logs.WithCreated(logs.NanoLayout), logs.WithLevels(logs.AllLevels()...)) } - if o.Mimetypes == nil { - o.Mimetypes = []*Mimetype{ - {Name: header.JSON, Marshal: json.Marshal, Unmarshal: json.Unmarshal, Problem: "application/problem+json"}, - {Name: header.XML, Marshal: xml.Marshal, Unmarshal: xml.Unmarshal, Problem: ""}, - } + if o.Codec == nil { + o.Codec = web.NewCodec(). + AddMimetype(json.Mimetype, json.Marshal, json.Unmarshal, json.ProblemMimetype). + AddMimetype(xml.Mimetype, xml.Marshal, xml.Unmarshal, "") } return o @@ -144,7 +136,7 @@ func TestHTTPServer_Serve(t *testing.T) { func TestHTTPServer_Serve_HTTPS(t *testing.T) { a := assert.New(t, false) - cert, err := tls.LoadX509KeyPair("./testdata/cert.pem", "./testdata/key.pem") + cert, err := tls.LoadX509KeyPair("./config/testdata/cert.pem", "./config/testdata/key.pem") a.NotError(err).NotNil(cert) srv := newTestServer(a, &Options{ HTTPServer: &http.Server{ @@ -397,7 +389,7 @@ func TestNewGateway(t *testing.T) { Cache: c, HTTPServer: &http.Server{Addr: ":8080"}, Registry: reg, - Mapper: Mapper{ + Mapper: map[string]group.Matcher{ "s1": group.NewPathVersion("", "/s1"), "s2": group.NewPathVersion("", "/s2"), }, diff --git a/server/testdata/.gitignore b/server/testdata/.gitignore deleted file mode 100644 index 397b4a76..00000000 --- a/server/testdata/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.log diff --git a/server/testdata/test.go b/server/testdata/test.go deleted file mode 100644 index 805e7bc3..00000000 --- a/server/testdata/test.go +++ /dev/null @@ -1,5 +0,0 @@ -// SPDX-License-Identifier: MIT - -package testdata - -// 测试文件 diff --git a/web.go b/web.go index cdd3365e..9ae4e83e 100644 --- a/web.go +++ b/web.go @@ -48,7 +48,7 @@ type ( // MarshalFunc 序列化函数原型 // - // NOTE: 自定义的 MarshalFunc 需要自行决定是否要自定义输出 [Problem] 和 [server.Render200Response]。 + // NOTE: 自定义的 MarshalFunc 需要自行决定是否要自定义输出 [Problem] 和 [server.RenderResponse]。 // // NOTE: MarshalFunc 的作用是输出内容,所以在实现中不能调用 [Context.Render] 等输出方法。 //