Swagger Plugin
ここを見ながら頑張ります。
https://github.com/favclip/ucon/tree/master/sample/swagger
まとめ
- 難しい!!
- [serverName]/api/swagger.jsonでJSONファイルを取得できる
- swagger.NewHandlerInfoでhandlerをwrapしよう
- swagger.NewPlugin(*Options)で作成
- ucon.DefaultMux.Prepare()でPlugin起動
- swagger.NewHandlerInfoでwrapしたものはucon.HandlerFuncではなくucon.Handleで登録する!
- swagger-uiで見るにはすべてのAPIでJSONを返す必要がある?(HTTP Statusのみでも)
Swagger Pluginを作成する
// NewPlugin returns new swagger plugin configured with the options.
func NewPlugin(opts *Options) *Plugin {
if opts == nil {
opts = &Options{}
}
plugin := &Plugin{
options: opts,
swagger: opts.Object,
typeSchemaMapper: make(map[reflect.Type]*TypeSchema),
}
for k, v := range DefaultTypeSchemaMapper {
plugin.typeSchemaMapper[k] = v
}
var _ ucon.HandlersScannerPlugin = plugin
return plugin
}
&swagger.Option
を引数にして新しいPluginを返しています。
DefaultTypeSchemaMapper
から定義済のschemaを登録しています。
var _ ucon.HandlersScannerPlugin = plugin
はGoでよく見る、右辺が左辺のInterfaceを実装しているかを確認する処理です。
swagger.Option.Object
type Options struct {
Object *Object
DefinitionNameModifier func(refT reflect.Type, defName string) string
}
// Object is https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#swagger-object
type Object struct {
Swagger string `json:"swagger" swagger:",req,d=2.0"`
Info *Info `json:"info" swagger:",req"`
Host string `json:"host,omitempty"`
BasePath string `json:"basePath,omitempty"`
Schemes []string `json:"schemes,omitempty" swagger:",d=https"`
Consumes []string `json:"consumes,omitempty" swagger:",d=application/json"`
Produces []string `json:"produces,omitempty" swagger:",d=application/json"`
Paths Paths `json:"paths" swagger:",req"`
Definitions Definitions `json:"definitions,omitempty"`
Parameters ParametersDefinitions `json:"parameters,omitempty"`
Responses ResponsesDefinitions `json:"responses,omitempty"`
SecurityDefinitions SecurityDefinitions `json:"securityDefinitions,omitempty"`
Security []SecurityRequirement `json:"security,omitempty"`
Tags []*Tag `json:"tags,omitempty"`
ExternalDocs *ExternalDocumentation `json:"externalDocs,omitempty"`
}
swaggerで定義されているSchemaのObjectでしょうかね。。
Pluginを登録
///// core.go /////
// Plugin can append Plugin to ServeMux.
func Plugin(plugin interface{}) {
DefaultMux.Plugin(plugin)
}
// Plugin can append Plugin to ServeMux.
func (m *ServeMux) Plugin(plugin interface{}) {
p, ok := plugin.(*pluginContainer)
if !ok {
p = &pluginContainer{base: plugin}
}
p.check()
m.plugins = append(m.plugins, p)
}
///// plugin.go /////
type pluginContainer struct {
base interface{}
}
func (p *pluginContainer) check() {
if p.HandlersScanner() != nil {
return
}
panic(fmt.Sprintf("unused plugin: %#v", p.base))
}
// HandlersScanner returns itself if it implements HandlersScannerPlugin.
func (p *pluginContainer) HandlersScanner() HandlersScannerPlugin {
if v, ok := p.base.(HandlersScannerPlugin); ok {
return v
}
return nil
}
// HandlersScannerPlugin is an interface to make a plugin for scanning request handlers.
type HandlersScannerPlugin interface {
HandlersScannerProcess(m *ServeMux, rds []*RouteDefinition) error
}
全体的に登録したいPluginがHandlersScannerPlugin
を実装していればPluginに登録可能のようです。
Pluginを使う
ucon.DefautlMux.Prepare()
を実行します
// Prepare the ServeMux.
// Plugin is not show affect to anything.
// This method is enabled plugins.
func (m *ServeMux) Prepare() {
for _, plugin := range m.plugins {
used := false
if sc := plugin.HandlersScanner(); sc != nil {
err := sc.HandlersScannerProcess(m, m.router.handlers)
if err != nil {
panic(err)
}
used = true
}
if !used {
panic(fmt.Sprintf("unused plugin: %#v", plugin))
}
}
}
先ほど登録したpluginsを実行しています。
plugin.HandlersScanner() == nil
の場合はused == false
のままなのでpanicするようです。
Swagger PluginのHandlersScannerProcess(m, m.router.handlers)を見る
Pluginが実行されるのは、HandlersScannerProcess(m, m.router.handlers)
なので、
Swagger Pluginで実装されている箇所を見ていきます。
// HandlersScannerProcess executes scanning all registered handlers to serve swagger.json.
func (p *Plugin) HandlersScannerProcess(m *ucon.ServeMux, rds []*ucon.RouteDefinition) error {
// construct swagger.json
for _, rd := range rds {
err := p.processHandler(rd)
if err != nil {
return err
}
}
err := p.swagger.finish()
if err != nil {
return err
}
// supply swagger.json endpoint
m.HandleFunc("GET", "/api/swagger.json", func(w http.ResponseWriter, r *http.Request) *Object {
return p.swagger
})
return nil
}
p.processHandler(*ucon.RouteDefinition)
→p.swagger.finish()
→m.HandlerFunc
の順に実行されています。
順番に見ていきます。
func (p *Plugin) processHandler(rd *ucon.RouteDefinition) error {
if p.swagger == nil {
p.swagger = &Object{}
}
if p.swagger.Paths == nil {
p.swagger.Paths = make(Paths, 0)
}
if p.swagger.Definitions == nil {
p.swagger.Definitions = make(Definitions, 0)
}
item := p.swagger.Paths[rd.PathTemplate.PathTemplate]
if item == nil {
item = &PathItem{}
}
var putOperation func(op *Operation)
// putOperationの設定は省略
op, err := p.extractHandlerInfo(rd)
if err != nil {
return err
}
if op != nil {
p.swagger.Paths[rd.PathTemplate.PathTemplate] = item
putOperation(op)
for _, tsc := range p.typeSchemaMapper {
if !tsc.AllowRef {
continue
}
if tsc.RefName == "" {
return errors.New("Name is required")
}
_, ok := p.swagger.Definitions[tsc.RefName]
if !ok {
p.swagger.Definitions[tsc.RefName] = tsc.Schema
}
}
}
return nil
}
p.swaggerに各種設定を登録していることがわかります。
p.extractHandlerInfo(*ucon.RouteDfinition)
を見てみます。
func (p *Plugin) extractHandlerInfo(rd *ucon.RouteDefinition) (*Operation, error) {
// とても長いので省略・・・
}
まとめると、HandleFuncで登録したものがゴニョゴニョ処理されているようでした・・・(多分)
p.swagger.finish()
関係は長いのでコード内にインラインで記述していきます。
func (o *Object) finish() error {
return checkObject(reflect.ValueOf(o))
}
func checkObject(refV reflect.Value) error {
if refV.Kind() == reflect.Ptr {
refV = refV.Elem()
}
var checkNext func(fV reflect.Value) error
// checkNextの設定は省略
for i, numField := 0, refV.NumField(); i < numField; i++ {
sf := refV.Type().Field(i)
fV := refV.Field(i)
swaggerTag := NewTagSwagger(sf.Tag)
// `swagger:",d=hogehoge`のhogehogeがObjectのFieldに設定される
if d := swaggerTag.Default(); d != "" && ucon.IsEmpty(fV) {
err := ucon.SetValueFromString(fV, d)
if err != nil {
return err
}
}
// `swagger:"req"`のようにreqがあり、かつそのFieldが空の場合にエラー
// reqがついたもののうち、Pathsはucon.HandleFuncしていれば自動で登録されている
if swaggerTag.Required() && ucon.IsEmpty(fV) {
return fmt.Errorf("%s is required", sf.Name)
}
// `swagger:",enum=a|b|c"のようにa,b,cがstringであれば処理が走る
if enum := swaggerTag.Enum(); len(enum) != 0 && !ucon.IsEmpty(fV) {
if sf.Type.Kind() != reflect.String {
return fmt.Errorf("unsupported kind: %s", sf.Type.Kind().String())
}
str := fV.String()
found := false
for _, c := range enum {
if c == str {
found = true
break
}
}
if !found {
return fmt.Errorf("invalid value: %s in %s", str, sf.Name)
}
}
// checkNextはarray,slice,struct,mapなど入れ子の構造に使用される
// 特にstructの場合はcheckObject自体が再帰的に実行される
err := checkNext(fV)
if err != nil {
return err
}
}
return nil
}
最終的にHandlerFuncでp.swagger
を返しています。
なお、ucon.Middleware(ResponseMapper())
かucon.Orthodox()
しないと多分エラーとなるので注意。
さて、ここまで読んで、p.swagger.Swagger
に値が入っていないことに気がついた。
これはどこで定義されているかとsampleに戻ると、swagger.NewHandlerInfo
が怪しそうだと気づく。
それでは、見てみましょう・・
// NewHandlerInfo returns new HandlerInfo containing given handler function.
func NewHandlerInfo(handler interface{}) *HandlerInfo {
ucon.CheckFunction(handler)
return &HandlerInfo{
HandlerFunc: handler,
}
}
特に何もなかった。まあいいかw
NewHandlerInfo
をしないと、おそらくp.extractHandlerInfo(*ucon.RouteDfinition)
が機能しないと思われます。