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,omitempty | cache,omitempty | cache,omitempty | cacheConfig | 指定缓存对象
如果为空,则会采用内存作为缓存对象。
- |
compressions,omitempty | compressions,omitempty | compressions>compression,omitempty | compressConfig | 压缩的相关配置
- 如果为空,那么不支持压缩功能。
|
fileSerializers,omitempty | fileSerializers,omitempty | fileSerializers>fileSerializer,omitempty | string | 指定配置文件的序列化
可通过 [RegisterFileSerializer] 进行添加额外的序列化方法。默认可用为:
@@ -71,6 +69,8 @@ configconfigOf在项目正式运行
- json 支持 .json 后缀名的文件
如果为空,表示支持以上所有格式。
+ |
compressions,omitempty | compressions,omitempty | compressions>compression,omitempty | compressConfig | 压缩的相关配置
+ 如果为空,那么不支持压缩功能。
|
mimetypes,omitempty | mimetypes,omitempty | mimetypes>mimetype,omitempty | mimetypeConfig | 指定可用的 mimetype
如果为空,那么将不支持任何格式的内容输出。
|
idGenerator,omitempty | idGenerator,omitempty | idGenerator,omitempty | string | 唯一 ID 生成器
@@ -103,6 +103,7 @@ logsConfig
|
levels,omitempty | levels,omitempty | level,omitempty | logs.Level | 允许开启的通道
为空表示采用 [AllLevels]
|
std,omitempty | std,omitempty | std,attr,omitempty | bool | 是否接管标准库的日志
+ |
stackError,omitempty | stackError,omitempty | stackError,attr,omitempty | bool | 是否显示错误日志的调用堆栈
|
handlers | handlers | handlers>handler | logHandlerConfig | 日志输出对象的配置
为空表示 [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] 等输出方法。
//