diff --git a/go.mod b/go.mod index 04a3fbd..1e178de 100644 --- a/go.mod +++ b/go.mod @@ -58,6 +58,9 @@ require ( github.com/redis/rueidis v1.0.66 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect + github.com/swaggest/jsonschema-go v0.3.74 // indirect + github.com/swaggest/openapi-go v0.2.60 // indirect + github.com/swaggest/refl v1.3.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect @@ -72,6 +75,7 @@ require ( golang.org/x/text v0.30.0 // indirect golang.org/x/tools v0.38.0 // indirect google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index 7dc0990..36687f5 100644 --- a/go.sum +++ b/go.sum @@ -213,6 +213,12 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/swaggest/jsonschema-go v0.3.74 h1:hkAZBK3RxNWU013kPqj0Q/GHGzYCCm9WcUTnfg2yPp0= +github.com/swaggest/jsonschema-go v0.3.74/go.mod h1:qp+Ym2DIXHlHzch3HKz50gPf2wJhKOrAB/VYqLS2oJU= +github.com/swaggest/openapi-go v0.2.60 h1:kglHH/WIfqAglfuWL4tu0LPakqNYySzklUWx06SjSKo= +github.com/swaggest/openapi-go v0.2.60/go.mod h1:jmFOuYdsWGtHU0BOuILlHZQJxLqHiAE6en+baE+QQUk= +github.com/swaggest/refl v1.3.1 h1:XGplEkYftR7p9cz1lsiwXMM2yzmOymTE9vneVVpaOh4= +github.com/swaggest/refl v1.3.1/go.mod h1:4uUVFVfPJ0NSX9FPwMPspeHos9wPFlCMGoPRllUbpvA= github.com/testcontainers/testcontainers-go v0.39.0 h1:uCUJ5tA+fcxbFAB0uP3pIK3EJ2IjjDUHFSZ1H1UxAts= github.com/testcontainers/testcontainers-go v0.39.0/go.mod h1:qmHpkG7H5uPf/EvOORKvS6EuDkBUPE3zpVGaH9NL7f8= github.com/testcontainers/testcontainers-go/modules/redis v0.39.0 h1:p54qELdCx4Gftkxzf44k9RJRRhaO/S5ehP9zo8SUTLM= @@ -280,6 +286,8 @@ google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/html/redoc.html b/html/redoc.html new file mode 100644 index 0000000..d455089 --- /dev/null +++ b/html/redoc.html @@ -0,0 +1,24 @@ + + + + Documentation of OstiweStatus API + + + + + + +
+ + + + \ No newline at end of file diff --git a/main.go b/main.go index 7376355..86da5c5 100644 --- a/main.go +++ b/main.go @@ -1,20 +1,34 @@ package main import ( + "embed" + "fmt" + "net/http" + "os" + "git.ostiwe.com/ostiwe-com/status/migration" appLog "git.ostiwe.com/ostiwe-com/status/modules/log" "git.ostiwe.com/ostiwe-com/status/pkg/args" + "git.ostiwe.com/ostiwe-com/status/router" "git.ostiwe.com/ostiwe-com/status/server" - _ "git.ostiwe.com/ostiwe-com/status/settings" + "git.ostiwe.com/ostiwe-com/status/settings" "github.com/alexflint/go-arg" + "github.com/gin-gonic/gin" ) +//go:embed html +var htmlFolder embed.FS + var appArgs args.AppArgs func main() { arg.MustParse(&appArgs) + defer appLog.Global.Get(appLog.SYSTEM).Debug("Exit from application") + if appArgs.Migration != nil && appArgs.Migration.Create != nil { + settings.Init() + if err := migration.CreateMigration(appArgs.Migration.Create.Name); err != nil { panic(err) } @@ -23,35 +37,111 @@ func main() { } if appArgs.Server != nil { - migration.RunMigration() + settings.Init() + migration.RunMigration() server.Run(appArgs.Server) + return } - // TODO: Rewrite to use gin router, instead of chi router - // if appArgs.ServerDocumentation != nil { - // appLog.Global.Get(appLog.SYSTEM).Info("Collect documentation") - // - // docs := router.Documentate() - // if !appArgs.ServerDocumentation.Plain { - // chiRouter := chi.NewRouter() - // - // err := docs.SetupRoutes(chiRouter, docs) - // if err != nil { - // appLog.Global.Get(appLog.SYSTEM).Error(fmt.Sprintf("Setup docs routes error: %v", err)) - // return - // } - // - // appLog.Global.Get(appLog.SYSTEM).Info(fmt.Sprintf("Start documentation server on port: %s", appArgs.ServerDocumentation.Port)) - // err = http.ListenAndServe(fmt.Sprintf(":%s", appArgs.ServerDocumentation.Port), chiRouter) - // if err != nil { - // appLog.Global.Get(appLog.SYSTEM).Error(fmt.Sprintf("Startup server error: %v", err)) - // } - // - // return - // } - // } + // TODO: Decompose document generation logic into separate methods + // Current code block handles both generation and serving logic - should be separated + if appArgs.Docs == nil { + return + } - appLog.Global.Get(appLog.SYSTEM).Info("Exit from application") + if appArgs.Docs.Generate != nil { + documentate, err := router.Documentate() + if err != nil { + appLog.Global.Get(appLog.SYSTEM).Error(err) + + return + } + + var file []byte + + switch appArgs.Docs.Generate.Format { + case "json": + file, err = documentate.MarshalJSON() + case "yaml": + file, err = documentate.MarshalYAML() + } + + if err != nil { + appLog.Global.Get(appLog.SYSTEM).Error(err) + + return + } + + if appArgs.Docs.Generate.Out == "stdout" { + _, err = os.Stdout.Write(file) + if err != nil { + appLog.Global.Get(appLog.SYSTEM).Error(err) + + return + } + + return + } + + err = os.WriteFile(appArgs.Docs.Generate.Out, file, os.ModeAppend) + if err != nil { + appLog.Global.Get(appLog.SYSTEM).Error(err) + + return + } + + return + } + + if appArgs.Docs.Serve != nil { + documentate, err := router.Documentate() + if err != nil { + appLog.Global.Get(appLog.SYSTEM).Error(err) + + return + } + docsJson, err := documentate.MarshalJSON() + if err != nil { + appLog.Global.Get(appLog.SYSTEM).Error(err) + + return + } + + html, err := htmlFolder.ReadFile("html/redoc.html") + if err != nil { + appLog.Global.Get(appLog.SYSTEM).Error(err) + + return + } + + g := gin.New() + g.Handle("GET", "/static-doc", func(c *gin.Context) { + c.Writer.Header().Add("Content-type", "application/json") + _, err = c.Writer.Write(docsJson) + if err != nil { + c.Writer.WriteHeader(http.StatusInternalServerError) + + return + } + }) + g.Handle("GET", "/docs/index.html", func(c *gin.Context) { + c.Writer.Header().Add("Content-Type", "text/html") + _, err = c.Writer.Write(html) + if err != nil { + c.Writer.WriteHeader(http.StatusInternalServerError) + + return + } + }) + + if err = g.Run(fmt.Sprintf(":%s", appArgs.Docs.Serve.Port)); err != nil { + appLog.Global.Get(appLog.SYSTEM).Error(err) + + return + } + + return + } } diff --git a/modules/jwt/jwt.go b/modules/jwt/jwt.go index 39df32c..d8511e3 100644 --- a/modules/jwt/jwt.go +++ b/modules/jwt/jwt.go @@ -21,7 +21,7 @@ var ( AuthMiddleware *ginJwt.GinJWTMiddleware ) -func init() { +func Init() { jwtPublicKeyPath := os.Getenv("JWT_SIGN_PUBLIC_KEY_PATH") if !strings.HasPrefix(jwtPublicKeyPath, "/") { jwtPublicKeyPath = settings.WorkingDir + "/" + jwtPublicKeyPath diff --git a/pkg/args/args.go b/pkg/args/args.go index 051093a..6ea16fa 100644 --- a/pkg/args/args.go +++ b/pkg/args/args.go @@ -3,7 +3,7 @@ package args import "git.ostiwe.com/ostiwe-com/status/version" type ServerCmd struct { - Port string `arg:"-p,--port" help:"Port to listen on" default:"8080"` + Port string `arg:"-p,--port,env:APP_PORT" help:"Port to listen on" default:"8080"` } type MigrationCreate struct { @@ -15,15 +15,23 @@ type Migration struct { } type ServerDocumentationCmd struct { - Port string `arg:"-p,--port" help:"Port to listen on" default:"8081"` - Plain bool `arg:"--plain" help:"Enable plain text output" default:"true"` - PlainFormat string `arg:"--plain-format" help:"Set format for output (json, yaml)" default:"yaml"` + Port string `arg:"-p,--port,env:APP_PORT_DOCS" help:"Port to listen on" default:"8081"` +} + +type GenerateDocumentationCmd struct { + Format string `arg:"--format,-f" help:"Set output format (json, yaml)" default:"yaml"` + Out string `arg:"--out,-o" help:"Output file name (or stdout)" default:"stdout"` +} + +type DocsCmd struct { + Serve *ServerDocumentationCmd `arg:"subcommand:serve" help:"Generate and serve the documentation server"` + Generate *GenerateDocumentationCmd `arg:"subcommand:generate" help:"Generate documentation to file"` } type AppArgs struct { - Server *ServerCmd `arg:"subcommand:server" help:"Start the api server"` - ServerDocumentation *ServerDocumentationCmd `arg:"subcommand:server-docs" help:"Generate documentation for api server and start documentation server"` - Migration *Migration `arg:"subcommand:migration" help:"Migration utils"` + Server *ServerCmd `arg:"subcommand:server" help:"Start the api server"` + Docs *DocsCmd `arg:"subcommand:docs" help:"Generate documentation to file or run documentation server"` + Migration *Migration `arg:"subcommand:migration" help:"Migration utils"` } func (AppArgs) Version() string { diff --git a/router/controller/controller.go b/router/controller/controller.go index 6e26ecd..9edb0cd 100644 --- a/router/controller/controller.go +++ b/router/controller/controller.go @@ -2,7 +2,8 @@ package controller import ( "github.com/gin-gonic/gin" - "github.com/go-andiamo/chioas" + "github.com/swaggest/openapi-go" + "github.com/swaggest/openapi-go/openapi3" ) // SecuredController - means, controller has middlewares @@ -12,7 +13,7 @@ type SecuredController interface { } type DocumentateController interface { - Documentate() (*chioas.Paths, *chioas.Components) + Documentate(r *openapi3.Reflector) openapi.OperationContext } type Controller interface { diff --git a/router/controller/ping/controller.go b/router/controller/ping/controller.go index ff7a0a8..084c712 100644 --- a/router/controller/ping/controller.go +++ b/router/controller/ping/controller.go @@ -6,7 +6,8 @@ import ( "git.ostiwe.com/ostiwe-com/status/modules/log" "git.ostiwe.com/ostiwe-com/status/router/controller" "github.com/gin-gonic/gin" - "github.com/go-andiamo/chioas" + "github.com/swaggest/openapi-go" + "github.com/swaggest/openapi-go/openapi3" ) type Controller struct { @@ -34,29 +35,19 @@ func (c *Controller) Handler() gin.HandlerFunc { } } -func (c *Controller) Documentate() (*chioas.Paths, *chioas.Components) { - return &chioas.Paths{ - "/ping": chioas.Path{ - Methods: map[string]chioas.Method{ - http.MethodGet: { - Handler: c.Handler(), - Responses: chioas.Responses{ - http.StatusOK: { - ContentType: "plain/text", - Schema: chioas.Schema{Type: "string"}, - Examples: chioas.Examples{ - { +func (c *Controller) Documentate(r *openapi3.Reflector) openapi.OperationContext { + op, err := r.NewOperationContext(c.Method(), c.Path()) + if err != nil { + return nil + } - Name: "200 OK", - Value: "pong", - }, - }, - }, - }, - }, - }, - Tag: "service", - Comment: "Route for check service is alive", - }, - }, nil + op.SetDescription("Route for check service is alive") + op.SetSummary("Server ping") + + op.AddRespStructure("pong", func(cu *openapi.ContentUnit) { + cu.ContentType = "plain/text" + cu.HTTPStatus = http.StatusOK + }) + + return op } diff --git a/router/server.go b/router/server.go index 7884efe..8a657a2 100644 --- a/router/server.go +++ b/router/server.go @@ -2,9 +2,9 @@ package router import ( "fmt" - "maps" "time" + "git.ostiwe.com/ostiwe-com/status/modules/jwt" "git.ostiwe.com/ostiwe-com/status/modules/log" "git.ostiwe.com/ostiwe-com/status/router/controller" "git.ostiwe.com/ostiwe-com/status/router/controller/auth" @@ -12,7 +12,7 @@ import ( "git.ostiwe.com/ostiwe-com/status/router/controller/service" "git.ostiwe.com/ostiwe-com/status/version" "github.com/gin-gonic/gin" - "github.com/go-andiamo/chioas" + "github.com/swaggest/openapi-go/openapi3" ) func getControllers() []controller.Controller { @@ -27,6 +27,8 @@ func InitRoutes() *gin.Engine { log.Global.Get(log.SERVER).Info("Setting up routers") startTime := time.Now() + jwt.Init() + r := gin.New() r.Use( gin.Recovery(), @@ -61,27 +63,16 @@ func InitRoutes() *gin.Engine { return r } -func Documentate() chioas.Definition { +func Documentate() (*openapi3.Spec, error) { ctrlList := getControllers() - apiDoc := chioas.Definition{ - AutoHeadMethods: true, - DocOptions: chioas.DocOptions{ - ServeDocs: true, - HideHeadMethods: true, - }, - Info: chioas.Info{ - Version: version.AppVersion(), - Title: "Status page API Documentation", - }, - Paths: make(chioas.Paths), - Components: &chioas.Components{ - Schemas: make(chioas.Schemas, 0), - Requests: make(chioas.CommonRequests), - Responses: make(chioas.CommonResponses), - Examples: make(chioas.Examples, 0), - Parameters: make(chioas.CommonParameters), - SecuritySchemes: make(chioas.SecuritySchemes, 0), - Extensions: make(chioas.Extensions), + + reflector := &openapi3.Reflector{ + Spec: &openapi3.Spec{ + Openapi: "3.0.3", + Info: openapi3.Info{ + Title: "OstiweStatus API", + Version: version.AppVersion(), + }, }, } @@ -91,25 +82,12 @@ func Documentate() chioas.Definition { continue } - documentatePaths, components := documentated.Documentate() - - if documentatePaths != nil { - for path, pathDoc := range *documentatePaths { - apiDoc.Paths[path] = pathDoc - } + err := reflector.AddOperation(documentated.Documentate(reflector)) + if err != nil { + return nil, err } - if components != nil { - apiDoc.Components.Schemas = append(apiDoc.Components.Schemas, components.Schemas...) - apiDoc.Components.Examples = append(apiDoc.Components.Examples, components.Examples...) - apiDoc.Components.SecuritySchemes = append(apiDoc.Components.SecuritySchemes, components.SecuritySchemes...) - - maps.Copy(apiDoc.Components.Requests, components.Requests) - maps.Copy(apiDoc.Components.Responses, components.Responses) - maps.Copy(apiDoc.Components.Parameters, components.Parameters) - maps.Copy(apiDoc.Components.Extensions, components.Extensions) - } } - return apiDoc + return reflector.Spec, nil } diff --git a/settings/settings.go b/settings/settings.go index ad15572..5817528 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -17,7 +17,9 @@ var ( func init() { appLog.SetGlobalManager(appLog.NewManager()) appLog.Global.Put(appLog.SYSTEM, logrus.New()) +} +func Init() { var err error WorkingDir, err = os.Getwd()