From be8cc8d79b4e7c680f31fed2f9fd6ccf19cc1a86 Mon Sep 17 00:00:00 2001 From: caixw Date: Tue, 3 Dec 2024 11:49:25 +0800 Subject: [PATCH] =?UTF-8?q?refactor(openapi):=20=E6=B7=BB=E5=8A=A0=20OpenA?= =?UTF-8?q?PISchema=20=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openapi/openapi.go | 31 +++++++++++- openapi/render.go | 2 +- openapi/render_test.go | 2 +- openapi/schema.go | 112 +++++++++++++++++++++++------------------ openapi/schema_test.go | 56 +++++++++++++++++++-- server.go | 2 +- service.go | 5 -- web.go | 2 +- 8 files changed, 150 insertions(+), 62 deletions(-) diff --git a/openapi/openapi.go b/openapi/openapi.go index c787e159..c69f7aaf 100644 --- a/openapi/openapi.go +++ b/openapi/openapi.go @@ -5,9 +5,34 @@ // Package openapi 采用 [web.Middleware] 中间件的形式生成 [openapi] 文档 // // 结构体标签 +// - [CommentTag] 用于可翻译的注释,该内容会被翻译后保存在字段的 Schema.Description 中; +// - openapi 对 openapi 类型的自定义,格式为 type,format,可以自定义字段的类型和格式; // -// - comment 用于可翻译的注释,该内容会被翻译后保存在字段的 Schema.Description 中; -// - openapi 对 openapi 类型的自定义,格式为 type,format,可以自定义字段的类型和格式; +// # Schema +// +// 大部分情况下,通过 [NewSchema] 可以将一个类型直接转换为 [Schema] 对象, +// 如果对类型中的字段有特殊要求,可以通过结构体标签 openapi 进行简单的自定义: +// +// type State int8 +// +// type x struct { +// State State `openapi:"string"` +// } +// +// 以上代码会将 State 解析为字符串类型,而不是默认的数值。 +// +// 当然对于复杂的需求,还可以通过实现 [OpenAPISchema] 接口实现自定义: +// +// type State int8 +// +// func(s State) OpenAPISchema(s *Schema) { +// s.Type = TypeString +// s.Enum = []string {"stopped", "running" } +// } +// +// 以上代码,会在解析 State 始终采用 OpenAPISchema 修改。 +// +// 结构体标签的优先级要高于 [OpenAPISchema] 接口。 // // [openapi]: https://spec.openapis.org/oas/v3.1.1.html package openapi @@ -414,6 +439,8 @@ func New(s web.Server, title web.LocaleStringer, o ...Option) *Document { } // Disable 是否禁用 [Document.Handler] 接口输出内容 +// +// 不影响内容的添加,但是会在调用 [Document.Handler] 时输出 404。 func (d *Document) Disable(disable bool) { d.disable = disable } // AddWebhook 添加 Webhook 的定义 diff --git a/openapi/render.go b/openapi/render.go index 39202dba..31119fed 100644 --- a/openapi/render.go +++ b/openapi/render.go @@ -65,7 +65,7 @@ func (o *openAPIRenderer) MarshalHTML() (name string, data any) { func (d *Document) Handler(tag ...string) web.HandlerFunc { return func(ctx *web.Context) web.Responser { if d.disable { - return ctx.NotImplemented() + return ctx.NotFound() } if m := ctx.Mimetype(false); (m != json.Mimetype && m != yaml.Mimetype && m != html.Mimetype) || diff --git a/openapi/render_test.go b/openapi/render_test.go index d7b2ea3c..63cdd34d 100644 --- a/openapi/render_test.go +++ b/openapi/render_test.go @@ -104,7 +104,7 @@ func TestDocument_Handler(t *testing.T) { d.Disable(true) servertest.Get(a, "http://localhost:8080/p/openapi").Header("accept", json.Mimetype). Do(nil). - Status(http.StatusNotImplemented) + Status(http.StatusNotFound) s.Close(500 * time.Millisecond) cancel() diff --git a/openapi/schema.go b/openapi/schema.go index 85e41a69..ab3ff1cd 100644 --- a/openapi/schema.go +++ b/openapi/schema.go @@ -14,6 +14,14 @@ import ( "github.com/issue9/web" ) +// OpenAPISchema 自定义某个类型在 openapi 文档中的类型 +type OpenAPISchema interface { + // OpenAPISchema 修改当前类型的 [Schema] 表示形式 + OpenAPISchema(s *Schema) +} + +var openAPISchemaType = reflect.TypeFor[OpenAPISchema]() + type Parameter struct { Ref *Ref @@ -163,6 +171,14 @@ var timeType = reflect.TypeFor[time.Time]() // desc 表示类型 t 的 Description 属性 // rootName 根结构体的名称,主要是为了解决子元素又引用了根元素的类型引起的循环引用。 func schemaFromType(d *Document, t reflect.Type, isRoot bool, rootName string, s *Schema) { + if t.Implements(openAPISchemaType) { + if t.Kind() == reflect.Pointer { // 值类型的指针符合 t.Implements,但是无法使用 reflect.New(t).Elem 获得一个有效的值。 + t = t.Elem() + } + reflect.New(t).Interface().(OpenAPISchema).OpenAPISchema(s) + return + } + for t.Kind() == reflect.Pointer { t = t.Elem() } @@ -219,70 +235,70 @@ func schemaFromObjectType(d *Document, t reflect.Type, isRoot bool, rootName str for i := 0; i < t.NumField(); i++ { f := t.Field(i) - k := f.Type.Kind() - var itemDesc web.LocaleStringer if f.Anonymous { schemaFromType(d, f.Type, isRoot, rootName, s) continue } - if f.IsExported() && k != reflect.Chan && k != reflect.Func && k != reflect.Complex64 && k != reflect.Complex128 { - name := f.Name - var xml *XML - if f.Tag != "" { - tag, omitempty, _ := getTagName(f, "json") - if tag == "-" { - continue - } else if tag != "" { - name = tag - } - - if !omitempty { - s.Required = append(s.Required, name) - } - - if xmlName, _, attr := getTagName(f, "xml"); xmlName != "" && xmlName != name { - xml = &XML{Name: xmlName, Attribute: attr} - } + if k := f.Type.Kind(); !f.IsExported() || k == reflect.Chan || k == reflect.Func || k == reflect.Complex64 || k == reflect.Complex128 { + continue + } - comment := f.Tag.Get(CommentTag) - if comment != "" { - itemDesc = web.Phrase(comment) - } + var itemDesc web.LocaleStringer + name := f.Name + var xml *XML + if f.Tag != "" { + tag, omitempty, _ := getTagName(f, "json") + if tag == "-" { + continue + } else if tag != "" { + name = tag + } - if tt := f.Tag.Get("openapi"); tt != "" { - if tt != "-" { - tags := strings.Split(tt, ",") - - format := "" - if len(tags) > 1 { - format = tags[1] - } - - s.Properties[name] = &Schema{ - Description: itemDesc, - Type: tags[0], - Format: format, - XML: xml, - } - } - continue - } + if !omitempty { + s.Required = append(s.Required, name) + } + if xmlName, _, attr := getTagName(f, "xml"); xmlName != "" && xmlName != name { + xml = &XML{Name: xmlName, Attribute: attr} } - item := &Schema{ - Description: itemDesc, - XML: xml, + comment := f.Tag.Get(CommentTag) + if comment != "" { + itemDesc = web.Phrase(comment) } - schemaFromType(d, t.Field(i).Type, false, rootName, item) - if item.Type == "" { + + if tt := f.Tag.Get("openapi"); tt != "" { + if tt != "-" { + tags := strings.Split(tt, ",") + + format := "" + if len(tags) > 1 { + format = tags[1] + } + + s.Properties[name] = &Schema{ + Description: itemDesc, + Type: tags[0], + Format: format, + XML: xml, + } + } continue } + } // end f.Tag - s.Properties[name] = item + item := &Schema{ + Description: itemDesc, + XML: xml, } + schemaFromType(d, t.Field(i).Type, false, rootName, item) + if item.Type == "" { + continue + } + + s.Properties[name] = item } } diff --git a/openapi/schema_test.go b/openapi/schema_test.go index 38954d02..14bd0be5 100644 --- a/openapi/schema_test.go +++ b/openapi/schema_test.go @@ -13,6 +13,13 @@ import ( "github.com/issue9/web" ) +type State int8 + +func (ss State) OpenAPISchema(s *Schema) { + s.Type = TypeString + s.Enum = []any{"1", "2"} +} + type schemaObject1 struct { object Root string @@ -20,12 +27,26 @@ type schemaObject1 struct { X string `openapi:"-"` Y string `openapi:"integer"` Z *object `openapi:"string,date"` + + S1 State `openapi:"integer" json:"s1"` + S2 *State `comment:"s2" json:"s2" xml:"S2"` + S3 State + + unExported bool } type schemaObject2 struct { schemaObject1 } +type schemaObject3 struct { + X int +} + +func (*schemaObject3) OpenAPISchema(s *Schema) { + s.Type = TypeString +} + func TestOfSchema(t *testing.T) { a := assert.New(t, false) @@ -70,12 +91,33 @@ func TestDocument_newSchema(t *testing.T) { Equal(s.Items.Type, TypeInteger). Equal(s.Default, []int{5, 6}) + a.Nil(d.newSchema(nil)) + s = d.newSchema(map[string]float32{"1": 3.2}) a.Equal(s.Type, TypeObject). Nil(s.Ref). NotNil(s.AdditionalProperties). Equal(s.AdditionalProperties.Type, TypeNumber) + // 指针实现 OpenAPISchema + s = d.newSchema(schemaObject3{}) + a.Equal(s.Type, TypeObject) + s = d.newSchema(&schemaObject3{}) + a.Equal(s.Type, TypeString) + so := &schemaObject3{} + s = d.newSchema(&so) // ** schemaObject{} + a.Equal(s.Type, TypeObject) + + // 值类型实现 OpenAPISchema + s = d.newSchema(State(5)) + a.Equal(s.Type, TypeString) + sss := State(5) + s = d.newSchema(&sss) + a.Equal(s.Type, TypeString) + ssss := &sss + s = d.newSchema(&ssss) // ** State + a.Equal(s.Type, TypeInteger) + s = d.newSchema(&object{}) a.Equal(s.Type, TypeObject). NotZero(s.Ref.Ref). @@ -90,19 +132,27 @@ func TestDocument_newSchema(t *testing.T) { a.Equal(s.Type, TypeObject). NotZero(s.Ref.Ref). Nil(s.Default). - Length(s.Properties, 7). + Length(s.Properties, 10). Equal(s.Properties["id"].Type, TypeInteger). Equal(s.Properties["Root"].Type, TypeString). Equal(s.Properties["T"].Type, TypeString). Equal(s.Properties["T"].Format, FormatDateTime). Equal(s.Properties["Y"].Type, TypeInteger). Equal(s.Properties["Z"].Type, TypeString). - Equal(s.Properties["Z"].Format, FormatDate) + Equal(s.Properties["Z"].Format, FormatDate). + Equal(s.Properties["s1"].Type, TypeInteger). // openapi 标签优先于 OpenAPISchema + Equal(s.Properties["s2"].Type, TypeString). // OpenAPISchema 接口 + Equal(s.Properties["s2"].XML.Name, "S2"). + Equal(s.Properties["s2"].Enum, []any{"1", "2"}). + Equal(s.Properties["s2"].Description, web.Phrase("s2")). // 注释可正确获取 + Equal(s.Properties["S3"].Type, TypeString). + Nil(s.Properties["S3"].XML). + Equal(s.Properties["S3"].Enum, []any{"1", "2"}) s = d.newSchema(schemaObject2{}) a.Equal(s.Type, TypeObject). NotZero(s.Ref.Ref). - Length(s.Properties, 7). + Length(s.Properties, 10). Equal(s.Properties["id"].Type, TypeInteger). Equal(s.Properties["Root"].Type, TypeString). Equal(s.Properties["T"].Type, TypeString). diff --git a/server.go b/server.go index dbb64d41..1834aa72 100644 --- a/server.go +++ b/server.go @@ -327,8 +327,8 @@ func (s *InternalServer) Close() { } } + s.doneErr = http.ErrServerClosed // 在 close 之前调用,以保证在 close 之后,doneErr 始终是正确的。 close(s.done) - s.doneErr = http.ErrServerClosed } func (s *InternalServer) Deadline() (time.Time, bool) { return time.Time{}, false } diff --git a/service.go b/service.go index 06259b58..69d9365c 100644 --- a/service.go +++ b/service.go @@ -52,11 +52,6 @@ type ( ServiceFunc func(context.Context) error // State 服务状态 - // - // 以下设置用于 restdoc - // - // @type string - // @enum stopped running failed State = scheduled.State Job = scheduled.Job diff --git a/web.go b/web.go index 602fb616..bb00216c 100644 --- a/web.go +++ b/web.go @@ -24,7 +24,7 @@ import ( ) // Version 当前框架的版本 -const Version = "0.99.0" +const Version = "0.100.0" type ( Logger = logs.Logger