From 5b14ae5885dd06d396346b6469db9be56702618b Mon Sep 17 00:00:00 2001 From: Ahmad Faiz Kamaludin Date: Tue, 12 May 2026 06:20:29 +0700 Subject: [PATCH 1/8] feat: add logger debug feature --- adapter/fiberv3openapi/router_test.go | 106 ++++-------------- adapter/fiberv3openapi/testdata/petstore.yaml | 44 ++++---- examples/petstore/dto.go | 8 +- examples/petstore/main.go | 2 +- examples/petstore/openapi.yaml | 16 +++ examples/petstore/router.go | 2 + internal/builder/builder.go | 8 ++ internal/builder/operation.go | 6 +- internal/reflect/reflector.go | 31 +++++ internal/testutil/dto/types.go | 4 +- openapi/config.go | 2 + option/openapi.go | 23 ++++ option/option_test.go | 19 ++++ router.go | 7 ++ 14 files changed, 162 insertions(+), 116 deletions(-) diff --git a/adapter/fiberv3openapi/router_test.go b/adapter/fiberv3openapi/router_test.go index db5a906..a4a5769 100644 --- a/adapter/fiberv3openapi/router_test.go +++ b/adapter/fiberv3openapi/router_test.go @@ -2,12 +2,10 @@ package fiberv3openapi_test import ( "io" - "mime/multipart" "net/http" "os" "path/filepath" "testing" - "time" "github.com/gofiber/fiber/v3" "github.com/stretchr/testify/assert" @@ -15,75 +13,13 @@ import ( stoplightemb "github.com/oaswrap/spec-ui/stoplightemb" "github.com/oaswrap/spec/internal/testutil" + "github.com/oaswrap/spec/internal/testutil/dto" "github.com/oaswrap/spec/openapi" "github.com/oaswrap/spec/option" "github.com/oaswrap/spec/adapter/fiberv3openapi" ) -type Pet struct { - ID int `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - Status string `json:"status" enum:"available,pending,sold"` - Category Category `json:"category"` - Tags []Tag `json:"tags"` - PhotoURLs []string `json:"photoUrls"` -} - -type Tag struct { - ID int `json:"id"` - Name string `json:"name"` -} - -type Category struct { - ID int `json:"id"` - Name string `json:"name"` -} - -type UpdatePetWithFormRequest struct { - ID int `path:"petId" required:"true"` - Name string `required:"true" formData:"name"` - Status string `formData:"status" enum:"available,pending,sold"` -} - -type UploadImageRequest struct { - ID int64 `params:"petId" path:"petId"` - AdditionalMetaData string `query:"additionalMetadata"` - _ *multipart.File `contentType:"application/octet-stream"` -} - -type DeletePetRequest struct { - ID int `path:"petId" required:"true"` - APIKey string `header:"api_key"` -} - -type Order struct { - ID int `json:"id"` - PetID int `json:"petId"` - Quantity int `json:"quantity"` - ShipDate time.Time `json:"shipDate"` - Status string `json:"status" enum:"placed,approved,delivered"` - Complete bool `json:"complete"` -} - -type PetUser struct { - ID int `json:"id"` - Username string `json:"username"` - FirstName string `json:"firstName"` - LastName string `json:"lastName"` - Email string `json:"email"` - Password string `json:"password"` - Phone string `json:"phone"` - UserStatus int `json:"userStatus" enum:"0,1,2"` -} - -type APIResponse struct { - Message string `json:"message"` - Type string `json:"type"` - Code int `json:"code"` -} - func PingHandler(c fiber.Ctx) error { return c.SendString("pong") } @@ -156,15 +92,15 @@ func TestRouter_Spec(t *testing.T) { option.OperationID("updatePet"), option.Summary("Update an existing pet"), option.Description("Update the details of an existing pet in the store."), - option.Request(new(Pet)), - option.Response(200, new(Pet)), + option.Request(new(dto.Pet)), + option.Response(200, new(dto.Pet)), ) pet.Post("/", nil).With( option.OperationID("addPet"), option.Summary("Add a new pet"), option.Description("Add a new pet to the store."), - option.Request(new(Pet)), - option.Response(201, new(Pet)), + option.Request(new(dto.Pet)), + option.Response(201, new(dto.Pet)), ) pet.Get("/findByStatus", nil).With( option.OperationID("findPetsByStatus"), @@ -173,7 +109,7 @@ func TestRouter_Spec(t *testing.T) { option.Request(new(struct { Status string `query:"status" enum:"available,pending,sold"` })), - option.Response(200, new([]Pet)), + option.Response(200, new([]dto.Pet)), ) pet.Get("/findByTags", nil).With( option.OperationID("findPetsByTags"), @@ -182,14 +118,14 @@ func TestRouter_Spec(t *testing.T) { option.Request(new(struct { Tags []string `query:"tags"` })), - option.Response(200, new([]Pet)), + option.Response(200, new([]dto.Pet)), ) pet.Post("/:petId/uploadImage", nil).With( option.OperationID("uploadFile"), option.Summary("Upload an image for a pet"), option.Description("Uploads an image for a pet."), - option.Request(new(UploadImageRequest)), - option.Response(200, new(APIResponse)), + option.Request(new(dto.UploadImageRequest)), + option.Response(200, new(dto.APIResponse)), ) pet.Get("/:petId", nil).With( option.OperationID("getPetById"), @@ -198,20 +134,20 @@ func TestRouter_Spec(t *testing.T) { option.Request(new(struct { ID int `uri:"petId" required:"true"` })), - option.Response(200, new(Pet)), + option.Response(200, new(dto.Pet)), ) pet.Post("/:petId", nil).With( option.OperationID("updatePetWithForm"), option.Summary("Update pet with form"), option.Description("Updates a pet in the store with form data."), - option.Request(new(UpdatePetWithFormRequest)), + option.Request(new(dto.UpdatePetWithFormRequest)), option.Response(200, nil), ) pet.Delete("/{petId}", nil).With( option.OperationID("deletePet"), option.Summary("Delete a pet"), option.Description("Delete a pet from the store by its ID."), - option.Request(new(DeletePetRequest)), + option.Request(new(dto.DeletePetRequest)), option.Response(204, nil), ) store := r.Group("/store").With( @@ -221,8 +157,8 @@ func TestRouter_Spec(t *testing.T) { option.OperationID("placeOrder"), option.Summary("Place an order"), option.Description("Place a new order for a pet."), - option.Request(new(Order)), - option.Response(201, new(Order)), + option.Request(new(dto.Order)), + option.Response(201, new(dto.Order)), ) store.Get("/order/:orderId", nil).With( option.OperationID("getOrderById"), @@ -231,7 +167,7 @@ func TestRouter_Spec(t *testing.T) { option.Request(new(struct { ID int `uri:"orderId" required:"true"` })), - option.Response(200, new(Order)), + option.Response(200, new(dto.Order)), option.Response(404, nil), ) store.Delete("/order/:orderId", nil).With( @@ -251,15 +187,15 @@ func TestRouter_Spec(t *testing.T) { option.OperationID("createUsersWithList"), option.Summary("Create users with list"), option.Description("Create multiple users in the store with a list."), - option.Request(new([]PetUser)), + option.Request(new([]dto.PetUser)), option.Response(201, nil), ) user.Post("/", nil).With( option.OperationID("createUser"), option.Summary("Create a new user"), option.Description("Create a new user in the store."), - option.Request(new(PetUser)), - option.Response(201, new(PetUser)), + option.Request(new(dto.PetUser)), + option.Response(201, new(dto.PetUser)), ) user.Get("/:username", nil).With( option.OperationID("getUserByName"), @@ -268,7 +204,7 @@ func TestRouter_Spec(t *testing.T) { option.Request(new(struct { Username string `uri:"username" required:"true"` })), - option.Response(200, new(PetUser)), + option.Response(200, new(dto.PetUser)), option.Response(404, nil), ) user.Put("/:username", nil).With( @@ -276,11 +212,11 @@ func TestRouter_Spec(t *testing.T) { option.Summary("Update an existing user"), option.Description("Update the details of an existing user."), option.Request(new(struct { - PetUser + dto.PetUser Username string `uri:"username" required:"true"` })), - option.Response(200, new(PetUser)), + option.Response(200, new(dto.PetUser)), option.Response(404, nil), ) user.Delete("/:username", nil).With( diff --git a/adapter/fiberv3openapi/testdata/petstore.yaml b/adapter/fiberv3openapi/testdata/petstore.yaml index 0a88526..04c8dff 100644 --- a/adapter/fiberv3openapi/testdata/petstore.yaml +++ b/adapter/fiberv3openapi/testdata/petstore.yaml @@ -39,14 +39,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Fiberv3openapi_testPet' + $ref: '#/components/schemas/DtoPet' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/Fiberv3openapi_testPet' + $ref: '#/components/schemas/DtoPet' security: - petstore_auth: - write:pets @@ -61,14 +61,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Fiberv3openapi_testPet' + $ref: '#/components/schemas/DtoPet' responses: '201': description: Created content: application/json: schema: - $ref: '#/components/schemas/Fiberv3openapi_testPet' + $ref: '#/components/schemas/DtoPet' security: - petstore_auth: - write:pets @@ -97,7 +97,7 @@ paths: schema: type: array items: - $ref: '#/components/schemas/Fiberv3openapi_testPet' + $ref: '#/components/schemas/DtoPet' security: - petstore_auth: - write:pets @@ -124,7 +124,7 @@ paths: schema: type: array items: - $ref: '#/components/schemas/Fiberv3openapi_testPet' + $ref: '#/components/schemas/DtoPet' security: - petstore_auth: - write:pets @@ -149,7 +149,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Fiberv3openapi_testPet' + $ref: '#/components/schemas/DtoPet' security: - petstore_auth: - write:pets @@ -222,7 +222,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Fiberv3openapi_testAPIResponse' + $ref: '#/components/schemas/DtoAPIResponse' security: - petstore_auth: - write:pets @@ -238,14 +238,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Fiberv3openapi_testOrder' + $ref: '#/components/schemas/DtoOrder' responses: '201': description: Created content: application/json: schema: - $ref: '#/components/schemas/Fiberv3openapi_testOrder' + $ref: '#/components/schemas/DtoOrder' /store/order/{orderId}: get: tags: @@ -266,7 +266,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Fiberv3openapi_testOrder' + $ref: '#/components/schemas/DtoOrder' '404': description: Not Found delete: @@ -296,14 +296,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Fiberv3openapi_testPetUser' + $ref: '#/components/schemas/DtoPetUser' responses: '201': description: Created content: application/json: schema: - $ref: '#/components/schemas/Fiberv3openapi_testPetUser' + $ref: '#/components/schemas/DtoPetUser' /user/createWithList: post: tags: @@ -317,7 +317,7 @@ paths: schema: type: array items: - $ref: '#/components/schemas/Fiberv3openapi_testPetUser' + $ref: '#/components/schemas/DtoPetUser' responses: '201': description: Created @@ -340,7 +340,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Fiberv3openapi_testPetUser' + $ref: '#/components/schemas/DtoPetUser' '404': description: Not Found put: @@ -389,7 +389,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Fiberv3openapi_testPetUser' + $ref: '#/components/schemas/DtoPetUser' '404': description: Not Found delete: @@ -409,7 +409,7 @@ paths: description: No Content components: schemas: - Fiberv3openapi_testAPIResponse: + DtoAPIResponse: type: object properties: code: @@ -419,7 +419,7 @@ components: type: string type: type: string - Fiberv3openapi_testOrder: + DtoOrder: type: object properties: complete: @@ -442,7 +442,7 @@ components: - placed - approved - delivered - Fiberv3openapi_testPet: + DtoPet: type: object properties: category: @@ -471,10 +471,10 @@ components: tags: type: array items: - $ref: '#/components/schemas/Fiberv3openapi_testTag' + $ref: '#/components/schemas/DtoTag' type: type: string - Fiberv3openapi_testPetUser: + DtoPetUser: type: object properties: email: @@ -499,7 +499,7 @@ components: - 2 username: type: string - Fiberv3openapi_testTag: + DtoTag: type: object properties: id: diff --git a/examples/petstore/dto.go b/examples/petstore/dto.go index 1d43f90..29afa9d 100644 --- a/examples/petstore/dto.go +++ b/examples/petstore/dto.go @@ -26,9 +26,9 @@ type Category struct { } type UpdatePetWithFormRequest struct { - ID int `path:"petId" required:"true"` - Name string `formData:"name" required:"true"` - Status string `formData:"status" enum:"available,pending,sold"` + ID int `path:"petId" validate:"required"` + Name string `form:"name" validate:"required"` + Status string `form:"status" enum:"available,pending,sold"` } type UploadImageRequest struct { @@ -38,7 +38,7 @@ type UploadImageRequest struct { } type DeletePetRequest struct { - ID int `path:"petId" required:"true"` + ID int `path:"petId" validate:"required"` ApiKey string `header:"api_key"` } diff --git a/examples/petstore/main.go b/examples/petstore/main.go index a93c697..210939e 100644 --- a/examples/petstore/main.go +++ b/examples/petstore/main.go @@ -67,7 +67,7 @@ func main() { option.OperationID("updatePetWithForm"), option.Summary("Update pet with form"), option.Description("Updates a pet in the store with form data."), - option.Request(new(UpdatePetWithFormRequest)), + option.Request(new(UpdatePetWithFormRequest), option.ContentType("application/x-www-form-urlencoded")), option.Response(200, nil), ) pet.Delete("/{petId}", diff --git a/examples/petstore/openapi.yaml b/examples/petstore/openapi.yaml index 3e5bf53..74fe8d5 100644 --- a/examples/petstore/openapi.yaml +++ b/examples/petstore/openapi.yaml @@ -168,6 +168,22 @@ paths: schema: type: integer format: int32 + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - name + properties: + name: + type: string + status: + type: string + enum: + - available + - pending + - sold responses: '200': description: OK diff --git a/examples/petstore/router.go b/examples/petstore/router.go index b53476a..0d28ee7 100644 --- a/examples/petstore/router.go +++ b/examples/petstore/router.go @@ -56,6 +56,8 @@ func createRouter() spec.Generator { ), option.WithSecurity("api_key", option.SecurityAPIKey("api_key", openapi.SecuritySchemeAPIKeyInHeader)), option.WithStripTrailingSlash(), + option.WithReflectorConfig(option.RequiredPropByValidateTag()), + option.WithDebugLogger(), ) return r diff --git a/internal/builder/builder.go b/internal/builder/builder.go index f5a643e..ac91107 100644 --- a/internal/builder/builder.go +++ b/internal/builder/builder.go @@ -1,6 +1,8 @@ package builder import ( + "io" + "log/slog" "regexp" "strings" @@ -19,6 +21,9 @@ type Builder struct { var pathParamTemplateRe = regexp.MustCompile(`\{([^{}]+)\}`) func NewBuilder(cfg *openapi.Config, doc *openapi.Document) *Builder { + if cfg.Logger == nil { + cfg.Logger = slog.New(slog.NewTextHandler(io.Discard, nil)) + } return &Builder{ Config: cfg, Doc: doc, @@ -31,6 +36,7 @@ func (b *Builder) AddOperation(method, path string, opts []option.OperationOptio } func (b *Builder) AddWebhookOperation(method, name string, opts []option.OperationOption) error { + b.Config.Logger.Debug("adding webhook operation", "method", method, "name", name) if reflect.IsOpenAPI30(b.Config.OpenAPIVersion) { return validate.Errorf("webhooks require OpenAPI 3.1.x or 3.2.0") } @@ -45,6 +51,7 @@ func (b *Builder) AddOperationTo( opts []option.OperationOption, items map[string]*openapi.PathItem, ) error { + b.Config.Logger.Debug("adding operation", "method", method, "target", target) cfg := &option.OperationConfig{} for _, opt := range opts { opt(cfg) @@ -168,6 +175,7 @@ func (b *Builder) ensurePathParameters(target string, op *openapi.Operation) { if _, ok := existing[name]; ok { continue } + b.Config.Logger.Debug("auto-injecting path parameter", "target", target, "param", name) op.Parameters = append(op.Parameters, &openapi.Parameter{ Name: name, In: string(openapi.ParameterInPath), diff --git a/internal/builder/operation.go b/internal/builder/operation.go index 9f8143d..ace76a7 100644 --- a/internal/builder/operation.go +++ b/internal/builder/operation.go @@ -9,19 +9,20 @@ import ( ) func (b *Builder) AddRequest(op *openapi.Operation, cu *openapi.ContentUnit) error { - params, body, err := b.Reflector.RequestParts(cu.Structure, ContentType(cu)) + ct := ContentType(cu) + params, body, err := b.Reflector.RequestParts(cu.Structure, ct) if err != nil { return err } op.Parameters = append(op.Parameters, params...) - ct := ContentType(cu) if body == nil { isDefaultJSON := ct == "application/json" || cu.ContentType == "" if isDefaultJSON && cu.Format == "" && cu.Example == nil && len(cu.Examples) == 0 { return nil } } + b.Config.Logger.Debug("building request body", "contentType", ct) if op.RequestBody == nil { op.RequestBody = &openapi.RequestBody{Content: map[string]openapi.MediaType{}} @@ -57,6 +58,7 @@ func (b *Builder) AddResponse(op *openapi.Operation, cu *openapi.ContentUnit) er } else if cu.HTTPStatus == 0 { return validate.Errorf("HTTP status is required unless ContentDefault is set") } + b.Config.Logger.Debug("building response body", "status", key) response := op.Responses[key] if response == nil { diff --git a/internal/reflect/reflector.go b/internal/reflect/reflector.go index 2a8eb91..4c530b5 100644 --- a/internal/reflect/reflector.go +++ b/internal/reflect/reflector.go @@ -2,8 +2,11 @@ package reflect import ( "errors" + "io" + "log/slog" "reflect" "slices" + "strings" "time" "github.com/oaswrap/spec/openapi" @@ -25,6 +28,12 @@ type Reflector struct { } func NewReflector(cfg *openapi.Config) *Reflector { + if cfg == nil { + cfg = &openapi.Config{} + } + if cfg.Logger == nil { + cfg.Logger = slog.New(slog.NewTextHandler(io.Discard, nil)) + } r := &Reflector{ Config: cfg, Components: map[string]*openapi.Schema{}, @@ -53,6 +62,7 @@ func (r *Reflector) RequestParts( return nil, nil, nil } if mapped := r.TypeMapping[t]; mapped != nil { + r.Config.Logger.Debug("applying type mapping", "src", t.String(), "dst", mapped.String()) t = mapped } if t.Kind() != reflect.Struct || IsTime(t) { @@ -182,6 +192,7 @@ func (r *Reflector) SchemaForValue(value any, mode SchemaMode) (*openapi.Schema, //nolint:nestif // exposer path needs pre+post hook if schema := r.SchemaFromValueExposer(value); schema != nil { t := IndirectType(reflect.TypeOf(value)) + r.Config.Logger.Debug("using SchemaExposer bypass", "type", t.String()) interceptSchema := r.interceptSchemaFn() if interceptSchema != nil { preSchema := &openapi.Schema{} @@ -190,9 +201,11 @@ func (r *Reflector) SchemaForValue(value any, mode SchemaMode) (*openapi.Schema, return nil, err } if stop { + r.Config.Logger.Debug("interceptSchema: pre-build stopped", "type", t.String()) return preSchema, nil } params := openapi.InterceptSchemaParams{Type: t, Schema: schema, Processed: true} + r.Config.Logger.Debug("interceptSchema: post-build called", "type", t.String()) if _, err := interceptSchema(params); err != nil { return nil, err } @@ -212,6 +225,7 @@ func (r *Reflector) RefSchema(t reflect.Type) (*openapi.Schema, error) { } r.Generating[t] = true r.Components[name] = &openapi.Schema{} + r.Config.Logger.Debug("generating component schema", "name", name, "type", t.String()) interceptSchema := r.interceptSchemaFn() if interceptSchema != nil { stop, err := interceptSchema(openapi.InterceptSchemaParams{Type: t, Schema: r.Components[name]}) @@ -221,6 +235,7 @@ func (r *Reflector) RefSchema(t reflect.Type) (*openapi.Schema, error) { return nil, err } if stop { + r.Config.Logger.Debug("interceptSchema: pre-build stopped", "type", t.String(), "component", name) delete(r.Generating, t) return &openapi.Schema{Ref: "#/components/schemas/" + name}, nil } @@ -238,6 +253,7 @@ func (r *Reflector) RefSchema(t reflect.Type) (*openapi.Schema, error) { r.Components[name].Required = built.Required if interceptSchema != nil { postParams := openapi.InterceptSchemaParams{Type: t, Schema: r.Components[name], Processed: true} + r.Config.Logger.Debug("interceptSchema: post-build called", "type", t.String(), "component", name) if _, err := interceptSchema(postParams); err != nil { delete(r.Generating, t) delete(r.Components, name) @@ -277,6 +293,7 @@ func (r *Reflector) StructSchema( name = LowerCamel(field.Name) } if interceptProp != nil { + r.Config.Logger.Debug("interceptProp: field hook called", "field", name, "parent", typeName(parentType)) if err := interceptProp(openapi.InterceptPropParams{ Name: name, Field: field, @@ -449,3 +466,17 @@ func (r *Reflector) SchemaFromTypeExposer(t reflect.Type) *openapi.Schema { func IsTime(t reflect.Type) bool { return t == reflect.TypeFor[time.Time]() } + +func typeName(t reflect.Type) string { + if t.Name() == "" { + return "" + } + pkg := t.PkgPath() + if idx := strings.LastIndex(pkg, "/"); idx >= 0 { + pkg = pkg[idx+1:] + } + if pkg != "" { + return pkg + "." + t.Name() + } + return t.Name() +} diff --git a/internal/testutil/dto/types.go b/internal/testutil/dto/types.go index eb8b180..d910a10 100644 --- a/internal/testutil/dto/types.go +++ b/internal/testutil/dto/types.go @@ -29,8 +29,8 @@ type Category struct { type UpdatePetWithFormRequest struct { ID int `path:"petId" required:"true"` - Name string `required:"true" formData:"name"` - Status string `formData:"status" enum:"available,pending,sold"` + Name string `required:"true" form:"name"` + Status string `form:"status" enum:"available,pending,sold"` } type UploadImageRequest struct { diff --git a/openapi/config.go b/openapi/config.go index 44813d9..4adcd8d 100644 --- a/openapi/config.go +++ b/openapi/config.go @@ -2,6 +2,7 @@ package openapi import ( "errors" + "log/slog" "reflect" specui "github.com/oaswrap/spec-ui" @@ -63,6 +64,7 @@ const ( // It contains all the necessary information and options to customize the generated document, including metadata, // server information, security schemes, and UI configuration. type Config struct { + Logger *slog.Logger OpenAPIVersion string Self string Title string diff --git a/option/openapi.go b/option/openapi.go index efeccc8..c725381 100644 --- a/option/openapi.go +++ b/option/openapi.go @@ -1,6 +1,10 @@ package option import ( + "io" + "log/slog" + "os" + specui "github.com/oaswrap/spec-ui" "github.com/oaswrap/spec-ui/config" "github.com/oaswrap/spec-ui/rapidoc" @@ -25,6 +29,7 @@ type OpenAPIOption func(*openapi.Config) // ) func WithOpenAPIConfig(opts ...OpenAPIOption) *openapi.Config { cfg := &openapi.Config{ + Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), OpenAPIVersion: openapi.Version312, Title: "API Documentation", Version: "1.0.0", @@ -481,3 +486,21 @@ func WithRapiDoc(cfg ...config.RapiDoc) OpenAPIOption { c.UIOption = rapidoc.WithUI(uiCfg) } } + +// WithLogger sets logger used for internal debug logs. Passing nil uses [slog.Default()]. +func WithLogger(logger *slog.Logger) OpenAPIOption { + return func(c *openapi.Config) { + if logger == nil { + c.Logger = slog.Default() + return + } + c.Logger = logger + } +} + +// WithDebugLogger enables debug-level logging to stderr using [slog.Default()]. +func WithDebugLogger() OpenAPIOption { + return func(c *openapi.Config) { + c.Logger = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug})) + } +} diff --git a/option/option_test.go b/option/option_test.go index 83cf53a..f568090 100644 --- a/option/option_test.go +++ b/option/option_test.go @@ -2,6 +2,7 @@ package option import ( "errors" + "log/slog" "reflect" "testing" @@ -673,3 +674,21 @@ func TestOptional(t *testing.T) { assert.Equal(t, "b", optional("a", "b")) assert.False(t, optional(true, false)) } + +func TestWithLogger(t *testing.T) { + t.Run("default logger is not nil", func(t *testing.T) { + cfg := WithOpenAPIConfig() + assert.NotNil(t, cfg.Logger) + }) + + t.Run("custom logger is stored", func(t *testing.T) { + custom := slog.Default() + cfg := WithOpenAPIConfig(WithLogger(custom)) + assert.Same(t, custom, cfg.Logger) + }) + + t.Run("nil logger falls back to slog.Default", func(t *testing.T) { + cfg := WithOpenAPIConfig(WithLogger(nil)) + assert.Same(t, slog.Default(), cfg.Logger) + }) +} diff --git a/router.go b/router.go index 445c714..0a29d8d 100644 --- a/router.go +++ b/router.go @@ -310,9 +310,11 @@ func (g *generator) build() { defer g.state.mu.Unlock() if !g.state.dirty { + g.cfg.Logger.Debug("skip build: document not dirty") return } + g.cfg.Logger.Debug("start build", "openapi_version", g.cfg.OpenAPIVersion) g.state.errs = nil g.state.doc = newDocument(g.cfg) g.state.builder = builder.NewBuilder(g.cfg, g.state.doc) @@ -360,6 +362,11 @@ func (g *generator) build() { } g.state.errs = append(g.state.errs, validate.ValidateDocument(g.state.doc, g.cfg.OpenAPIVersion)...) g.state.dirty = false + if len(g.state.errs) > 0 { + g.cfg.Logger.Warn("finish build", "routes", len(routes), "errors", len(g.state.errs)) + } else { + g.cfg.Logger.Debug("finish build", "routes", len(routes), "errors", 0) + } } type routeItem struct { From d4c3257494db7c26da910d4bc83e976b53ad4522 Mon Sep 17 00:00:00 2001 From: Ahmad Faiz Kamaludin Date: Tue, 12 May 2026 06:35:42 +0700 Subject: [PATCH 2/8] feat: auto set content type by struct if it not spesify set --- adapter/chiopenapi/testdata/petstore.yaml | 16 +++++++ adapter/echoopenapi/testdata/petstore.yaml | 16 +++++++ adapter/echov5openapi/testdata/petstore.yaml | 16 +++++++ adapter/fiberopenapi/testdata/petstore.yaml | 16 +++++++ adapter/fiberv3openapi/testdata/petstore.yaml | 16 +++++++ adapter/ginopenapi/testdata/petstore.yaml | 16 +++++++ adapter/httpopenapi/testdata/petstore.yaml | 16 +++++++ adapter/irisopenapi/testdata/petstore.yaml | 16 +++++++ adapter/muxopenapi/testdata/petstore.yaml | 16 +++++++ examples/petstore/main.go | 2 +- internal/builder/utils.go | 6 +++ internal/builder/utils_test.go | 15 ++++++ internal/reflect/utils.go | 47 +++++++++++++++++++ internal/reflect/utils_test.go | 34 ++++++++++++++ 14 files changed, 247 insertions(+), 1 deletion(-) diff --git a/adapter/chiopenapi/testdata/petstore.yaml b/adapter/chiopenapi/testdata/petstore.yaml index 655941f..11051d5 100644 --- a/adapter/chiopenapi/testdata/petstore.yaml +++ b/adapter/chiopenapi/testdata/petstore.yaml @@ -167,6 +167,22 @@ paths: schema: type: integer format: int32 + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - name + properties: + name: + type: string + status: + type: string + enum: + - available + - pending + - sold responses: '200': description: OK diff --git a/adapter/echoopenapi/testdata/petstore.yaml b/adapter/echoopenapi/testdata/petstore.yaml index ea3c059..70b190f 100644 --- a/adapter/echoopenapi/testdata/petstore.yaml +++ b/adapter/echoopenapi/testdata/petstore.yaml @@ -167,6 +167,22 @@ paths: schema: type: integer format: int32 + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - name + properties: + name: + type: string + status: + type: string + enum: + - available + - pending + - sold responses: '200': description: OK diff --git a/adapter/echov5openapi/testdata/petstore.yaml b/adapter/echov5openapi/testdata/petstore.yaml index ea3c059..70b190f 100644 --- a/adapter/echov5openapi/testdata/petstore.yaml +++ b/adapter/echov5openapi/testdata/petstore.yaml @@ -167,6 +167,22 @@ paths: schema: type: integer format: int32 + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - name + properties: + name: + type: string + status: + type: string + enum: + - available + - pending + - sold responses: '200': description: OK diff --git a/adapter/fiberopenapi/testdata/petstore.yaml b/adapter/fiberopenapi/testdata/petstore.yaml index 04c8dff..f84ec21 100644 --- a/adapter/fiberopenapi/testdata/petstore.yaml +++ b/adapter/fiberopenapi/testdata/petstore.yaml @@ -167,6 +167,22 @@ paths: schema: type: integer format: int32 + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - name + properties: + name: + type: string + status: + type: string + enum: + - available + - pending + - sold responses: '200': description: OK diff --git a/adapter/fiberv3openapi/testdata/petstore.yaml b/adapter/fiberv3openapi/testdata/petstore.yaml index 04c8dff..f84ec21 100644 --- a/adapter/fiberv3openapi/testdata/petstore.yaml +++ b/adapter/fiberv3openapi/testdata/petstore.yaml @@ -167,6 +167,22 @@ paths: schema: type: integer format: int32 + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - name + properties: + name: + type: string + status: + type: string + enum: + - available + - pending + - sold responses: '200': description: OK diff --git a/adapter/ginopenapi/testdata/petstore.yaml b/adapter/ginopenapi/testdata/petstore.yaml index 04c8dff..f84ec21 100644 --- a/adapter/ginopenapi/testdata/petstore.yaml +++ b/adapter/ginopenapi/testdata/petstore.yaml @@ -167,6 +167,22 @@ paths: schema: type: integer format: int32 + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - name + properties: + name: + type: string + status: + type: string + enum: + - available + - pending + - sold responses: '200': description: OK diff --git a/adapter/httpopenapi/testdata/petstore.yaml b/adapter/httpopenapi/testdata/petstore.yaml index 655941f..11051d5 100644 --- a/adapter/httpopenapi/testdata/petstore.yaml +++ b/adapter/httpopenapi/testdata/petstore.yaml @@ -167,6 +167,22 @@ paths: schema: type: integer format: int32 + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - name + properties: + name: + type: string + status: + type: string + enum: + - available + - pending + - sold responses: '200': description: OK diff --git a/adapter/irisopenapi/testdata/petstore.yaml b/adapter/irisopenapi/testdata/petstore.yaml index 04c8dff..f84ec21 100644 --- a/adapter/irisopenapi/testdata/petstore.yaml +++ b/adapter/irisopenapi/testdata/petstore.yaml @@ -167,6 +167,22 @@ paths: schema: type: integer format: int32 + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - name + properties: + name: + type: string + status: + type: string + enum: + - available + - pending + - sold responses: '200': description: OK diff --git a/adapter/muxopenapi/testdata/petstore.yaml b/adapter/muxopenapi/testdata/petstore.yaml index bc9973d..5d92906 100644 --- a/adapter/muxopenapi/testdata/petstore.yaml +++ b/adapter/muxopenapi/testdata/petstore.yaml @@ -192,6 +192,22 @@ paths: schema: type: integer format: int32 + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - name + properties: + name: + type: string + status: + type: string + enum: + - available + - pending + - sold responses: '200': description: OK diff --git a/examples/petstore/main.go b/examples/petstore/main.go index 210939e..a93c697 100644 --- a/examples/petstore/main.go +++ b/examples/petstore/main.go @@ -67,7 +67,7 @@ func main() { option.OperationID("updatePetWithForm"), option.Summary("Update pet with form"), option.Description("Updates a pet in the store with form data."), - option.Request(new(UpdatePetWithFormRequest), option.ContentType("application/x-www-form-urlencoded")), + option.Request(new(UpdatePetWithFormRequest)), option.Response(200, nil), ) pet.Delete("/{petId}", diff --git a/internal/builder/utils.go b/internal/builder/utils.go index b4f2790..43e4ce7 100644 --- a/internal/builder/utils.go +++ b/internal/builder/utils.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" + "github.com/oaswrap/spec/internal/reflect" "github.com/oaswrap/spec/internal/validate" "github.com/oaswrap/spec/openapi" ) @@ -114,6 +115,11 @@ func ContentType(cu *openapi.ContentUnit) string { if cu != nil && cu.ContentType != "" { return cu.ContentType } + if cu != nil && cu.Structure != nil { + if ct := reflect.InferContentType(cu.Structure); ct != "" { + return ct + } + } return "application/json" } diff --git a/internal/builder/utils_test.go b/internal/builder/utils_test.go index d7c7b68..f883059 100644 --- a/internal/builder/utils_test.go +++ b/internal/builder/utils_test.go @@ -1,6 +1,7 @@ package builder import ( + "mime/multipart" "net/http" "testing" @@ -55,9 +56,23 @@ func TestSetOperation(t *testing.T) { } func TestContentType(t *testing.T) { + type formBody struct { + Name string `form:"name"` + } + type fileBody struct { + File *multipart.FileHeader `form:"file"` + } + assert.Equal(t, "application/json", ContentType(nil)) assert.Equal(t, "application/json", ContentType(&openapi.ContentUnit{})) assert.Equal(t, "application/xml", ContentType(&openapi.ContentUnit{ContentType: "application/xml"})) + assert.Equal(t, "application/x-www-form-urlencoded", ContentType(&openapi.ContentUnit{Structure: formBody{}})) + assert.Equal(t, "multipart/form-data", ContentType(&openapi.ContentUnit{Structure: fileBody{}})) + // explicit ContentType always wins over struct tag inference + assert.Equal(t, "application/json", ContentType(&openapi.ContentUnit{ + ContentType: "application/json", + Structure: formBody{}, + })) } func TestResponseDescription(t *testing.T) { diff --git a/internal/reflect/utils.go b/internal/reflect/utils.go index 273a3c2..ff185fd 100644 --- a/internal/reflect/utils.go +++ b/internal/reflect/utils.go @@ -2,6 +2,7 @@ package reflect import ( "fmt" + "mime/multipart" "path" "reflect" "regexp" @@ -150,3 +151,49 @@ func SanitizeTypeName(name string) string { // Final cleanup for any remaining characters return genericNameRe.ReplaceAllString(name, "") } + +var ( + typeFileHeader = reflect.TypeFor[multipart.FileHeader]() + typeFile = reflect.TypeFor[multipart.File]() +) + +// InferContentType inspects struct tags on value to determine request body content type. +// Returns "multipart/form-data" if any field tagged `form` holds a file type, +// "application/x-www-form-urlencoded" if any `form` tag is present, or "" otherwise. +func InferContentType(value any) string { + t := IndirectType(reflect.TypeOf(value)) + if t == nil || t.Kind() != reflect.Struct || IsTime(t) { + return "" + } + hasForm := false + hasFile := false + ForEachField(t, func(field reflect.StructField) { + if TagName(field, "form") == "" { + return + } + hasForm = true + if isFileField(field.Type) { + hasFile = true + } + }) + if !hasForm { + return "" + } + if hasFile { + return "multipart/form-data" + } + return "application/x-www-form-urlencoded" +} + +func isFileField(t reflect.Type) bool { + if t.Kind() == reflect.Pointer { + t = t.Elem() + } + if t.Kind() == reflect.Slice { + t = t.Elem() + if t.Kind() == reflect.Pointer { + t = t.Elem() + } + } + return t == typeFileHeader || t == typeFile || t.Implements(typeFile) +} diff --git a/internal/reflect/utils_test.go b/internal/reflect/utils_test.go index 96e7747..74db1f4 100644 --- a/internal/reflect/utils_test.go +++ b/internal/reflect/utils_test.go @@ -1,6 +1,7 @@ package reflect import ( + "mime/multipart" "reflect" "testing" "time" @@ -113,3 +114,36 @@ func TestInternalHelpers(t *testing.T) { assert.Equal(t, []string{"a", "b", "c"}, uniqueStrings([]string{"a", "b", "a", "c", "b"})) }) } + +func TestInferContentType(t *testing.T) { + type noTags struct { + Name string `json:"name"` + } + type formBody struct { + Name string `form:"name"` + Email string `form:"email"` + } + type mixedBody struct { + Query string `query:"q"` + Name string `form:"name"` + } + type fileBody struct { + File *multipart.FileHeader `form:"file"` + } + type multiFileBody struct { + Files []*multipart.FileHeader `form:"files"` + } + type fileInterfaceBody struct { + File multipart.File `form:"file"` + } + + assert.Empty(t, InferContentType(nil)) + assert.Empty(t, InferContentType(noTags{})) + assert.Empty(t, InferContentType("string")) + assert.Equal(t, "application/x-www-form-urlencoded", InferContentType(formBody{})) + assert.Equal(t, "application/x-www-form-urlencoded", InferContentType(&formBody{})) + assert.Equal(t, "application/x-www-form-urlencoded", InferContentType(mixedBody{})) + assert.Equal(t, "multipart/form-data", InferContentType(fileBody{})) + assert.Equal(t, "multipart/form-data", InferContentType(multiFileBody{})) + assert.Equal(t, "multipart/form-data", InferContentType(fileInterfaceBody{})) +} From 52e134393a0130ad12c99ee2534477675c752acb Mon Sep 17 00:00:00 2001 From: Ahmad Faiz Kamaludin Date: Tue, 12 May 2026 06:44:29 +0700 Subject: [PATCH 3/8] docs: upadte changelog --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d7e7c8..d4664a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.5.1] - Unreleased + +### Added +- Logger debug feature for tracing spec generation behavior. +- `InterceptSchema` and `InterceptProp` reflector hooks for customizing schema and property generation. +- `RequiredPropByValidateTag` hook for marking fields required based on `validate` struct tags. +- `ParentType` field on `InterceptPropParams` for richer hook context. +- Automatic content-type detection from struct type when not explicitly set. + +### Fixed +- `uint8`/`uint16` reflected as `int32` format; `uint`/`uint32`/`uint64`/`uintptr` reflected as `int64` format. +- `InterceptSchema`/`InterceptProp` hook error propagation and correctness. +- `RefSchema` pre-hook: assign `StructSchema` fields onto existing pointer so pre-hook customizations (extensions, description) survive to the post-hook. +- `ApplyNullable` (OAS 3.1+): merge `"null"` into an existing `[]string` type slice instead of silently skipping. +- `Required` field deduplication to prevent duplicates when both `required` struct tag and `RequiredPropByValidateTag` are used simultaneously. +- `parentSnapshot` restore on `ErrSkipProperty` in post-hooks to roll back parent schema mutations (`AllOf`, `AnyOf`, `OneOf`, extensions). +- Lint violations and YAML tag reading in `MarshalYAML`. + ## [0.5.0] - 2026-05-11 ### Added @@ -78,6 +96,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Updated adapter dependency versions. +[0.5.1]: https://github.com/oaswrap/spec/compare/v0.5.0...HEAD [0.5.0]: https://github.com/oaswrap/spec/compare/v0.4.2...v0.5.0 [0.4.2]: https://github.com/oaswrap/spec/compare/v0.4.1...v0.4.2 [0.4.1]: https://github.com/oaswrap/spec/compare/v0.4.0...v0.4.1 From 34bb10b2bc7ff49c028f6127c0ad8e1cdfce48d9 Mon Sep 17 00:00:00 2001 From: Ahmad Faiz Kamaludin Date: Tue, 12 May 2026 07:17:32 +0700 Subject: [PATCH 4/8] feat: add TextMarshaler reflection and embedded allOf $ref support - Types implementing encoding.TextMarshaler+TextUnmarshaler (without json.Marshaler) are now reflected as type:string schemas. - Add openapi.EmbedReferencer interface and refer:"true" struct tag: embedded structs opt into allOf $ref instead of field inlining. - Fix RefSchema to propagate AllOf from StructSchema into component. - Add golden tests for embed_ref case across all three OAS versions. --- CHANGELOG.md | 2 + README.md | 33 ++++++++ golden_test.go | 41 ++++++++++ internal/reflect/converter.go | 33 +++++++- internal/reflect/converter_test.go | 120 +++++++++++++++++++++++++++++ internal/reflect/reflector.go | 17 ++++ internal/reflect/utils.go | 17 +++- openapi/config.go | 8 ++ testdata/embed_ref.v30.yaml | 64 +++++++++++++++ testdata/embed_ref.v31.yaml | 64 +++++++++++++++ testdata/embed_ref.v32.yaml | 64 +++++++++++++++ 11 files changed, 461 insertions(+), 2 deletions(-) create mode 100644 testdata/embed_ref.v30.yaml create mode 100644 testdata/embed_ref.v31.yaml create mode 100644 testdata/embed_ref.v32.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index d4664a0..91860aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `RequiredPropByValidateTag` hook for marking fields required based on `validate` struct tags. - `ParentType` field on `InterceptPropParams` for richer hook context. - Automatic content-type detection from struct type when not explicitly set. +- `encoding.TextMarshaler`/`TextUnmarshaler` support: types implementing both interfaces (without `json.Marshaler`) are reflected as `type: string`. +- `EmbedReferencer` interface and `refer:"true"` struct tag: embedded structs opt into `allOf $ref` instead of field inlining. ### Fixed - `uint8`/`uint16` reflected as `int32` format; `uint`/`uint32`/`uint64`/`uintptr` reflected as `int64` format. diff --git a/README.md b/README.md index 8aa7eeb..bb4330b 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ Code-first, framework-agnostic OpenAPI 3.x spec builder for Go. Generate docs fr - `InterceptSchema` hook for type-level schema customization and override. - `InterceptProp` hook for field-level property filtering and modification. - `RequiredPropByValidateTag` option to derive `required` from `validate` struct tags. +- `encoding.TextMarshaler`/`TextUnmarshaler` types automatically reflected as `type: string`. +- `EmbedReferencer` interface and `refer:"true"` tag for embedded struct `allOf $ref` instead of field inlining. --- @@ -438,6 +440,7 @@ type SearchRequest struct { | `map[string]T` | `type: object`, `additionalProperties: T` | | Structs | `type: object`, `properties` | | Named structs (component mode) | `#/components/schemas/{TypeName}` reference | +| `encoding.TextMarshaler` + `TextUnmarshaler` (no `json.Marshaler`) | `type: string` | | Pointers | Nullable schema behavior | Custom types can expose their own schema when tags are not expressive enough: @@ -455,6 +458,36 @@ func (*Slug) OpenAPISchema(version string) *openapi.Schema { For static schemas, implement `OpenAPISchema() *openapi.Schema` instead. Field tags are still applied on top of custom schemas. +### Embedded struct references + +By default, anonymous embedded struct fields are inlined into the parent schema. To emit an `allOf $ref` instead, use the `refer:"true"` tag or implement `openapi.EmbedReferencer` on the embedded type: + +```go +// Via struct tag +type Request struct { + Base `refer:"true"` + Name string `json:"name"` +} + +// Via interface (useful when you own the embedded type) +type Base struct { + ID int `json:"id"` +} + +func (Base) ReferEmbedded() {} +``` + +Both produce: +```yaml +Request: + type: object + properties: + name: + type: string + allOf: + - $ref: '#/components/schemas/Base' +``` + --- ## Reflector Configuration diff --git a/golden_test.go b/golden_test.go index b4c8ed2..15d6392 100644 --- a/golden_test.go +++ b/golden_test.go @@ -67,6 +67,33 @@ type QueryStringRequest struct { } `querystring:"payload"` } +// EmbedRef golden test types. + +type EmbedRefBase struct { + ID int `json:"id"` + Role string `json:"role"` +} + +type EmbedRefBaseViaInterface struct { + Tag string `json:"tag"` +} + +func (EmbedRefBaseViaInterface) ReferEmbedded() {} + +type EmbedRefRequest struct { + EmbedRefBase `refer:"true"` + + Name string `json:"name"` + Age int `json:"age"` +} + +type EmbedRefMixedRequest struct { + EmbedRefBaseViaInterface + + Code string `json:"code"` + Value int `json:"value"` +} + type mockPathParser struct{} func (mockPathParser) Parse(path string) (string, error) { @@ -154,6 +181,20 @@ func TestGolden(t *testing.T) { r.Post("/anonymous", option.Request(new(AnonymousStructRequest)), option.Response(204, nil)) }, }, + { + name: "embed_ref", + opts: []option.OpenAPIOption{option.WithTitle("EmbedRef API"), option.WithVersion("1.0.0")}, + run: func(r spec.Router) { + r.Post("/refer-tag", + option.Request(new(EmbedRefRequest)), + option.Response(200, new(EmbedRefBase)), + ) + r.Post("/refer-interface", + option.Request(new(EmbedRefMixedRequest)), + option.Response(204, nil), + ) + }, + }, { name: "nested_structures", run: func(r spec.Router) { diff --git a/internal/reflect/converter.go b/internal/reflect/converter.go index 2d0fc0c..20ea117 100644 --- a/internal/reflect/converter.go +++ b/internal/reflect/converter.go @@ -1,12 +1,27 @@ package reflect import ( + "encoding" + "encoding/json" "reflect" "slices" "github.com/oaswrap/spec/openapi" ) +var ( + typeOfTextMarshaler = reflect.TypeFor[encoding.TextMarshaler]() + typeOfTextUnmarshaler = reflect.TypeFor[encoding.TextUnmarshaler]() + typeOfJSONMarshaler = reflect.TypeFor[json.Marshaler]() +) + +func isTextMarshaler(t reflect.Type) bool { + impl := func(iface reflect.Type) bool { + return t.Implements(iface) || reflect.PointerTo(t).Implements(iface) + } + return impl(typeOfTextMarshaler) && impl(typeOfTextUnmarshaler) && !impl(typeOfJSONMarshaler) +} + //nolint:funlen,gocognit // covers full OpenAPI scalar/collection/struct mapping in one switch for readability. func (r *Reflector) SchemaForType( t reflect.Type, @@ -82,6 +97,22 @@ func (r *Reflector) SchemaForType( } } + if isTextMarshaler(t) { + schema := &openapi.Schema{Type: "string"} + if interceptSchema != nil { + if _, err := interceptSchema( + openapi.InterceptSchemaParams{Type: t, Schema: schema, Processed: true}, + ); err != nil { + return nil, err + } + } + r.ApplyNullable(schema, nullable) + if field != nil { + r.ApplySchemaTags(schema, *field) + } + return schema, nil + } + var schema *openapi.Schema switch t.Kind() { //nolint:exhaustive // only interested in types supported by OpenAPI case reflect.Bool: @@ -201,5 +232,5 @@ func IsOpenAPI30(version string) bool { } func IsComponentType(t reflect.Type) bool { - return t.Kind() == reflect.Struct && t.Name() != "" && !IsTime(t) + return t.Kind() == reflect.Struct && t.Name() != "" && !IsTime(t) && !isTextMarshaler(t) } diff --git a/internal/reflect/converter_test.go b/internal/reflect/converter_test.go index 53e0f90..f30d86f 100644 --- a/internal/reflect/converter_test.go +++ b/internal/reflect/converter_test.go @@ -15,6 +15,43 @@ import ( "github.com/oaswrap/spec/option" ) +// TextMarshaler test types. + +type textStatus int + +func (s textStatus) MarshalText() ([]byte, error) { return []byte("ok"), nil } +func (s *textStatus) UnmarshalText([]byte) error { return nil } + +type textStatusJSONOverride int + +func (s textStatusJSONOverride) MarshalText() ([]byte, error) { return []byte("ok"), nil } +func (s *textStatusJSONOverride) UnmarshalText([]byte) error { return nil } +func (s textStatusJSONOverride) MarshalJSON() ([]byte, error) { return []byte(`"ok"`), nil } + +// EmbedReferencer test types. + +type embedBase struct { + BaseField string `json:"base_field"` +} + +type embedRefViaTag struct { + embedBase `refer:"true"` + + Name string `json:"name"` +} + +type embedBaseViaInterface struct { + OtherField string `json:"other_field"` +} + +func (embedBaseViaInterface) ReferEmbedded() {} + +type embedRefViaInterface struct { + embedBaseViaInterface + + ID int `json:"id"` +} + type CustomSlug string func (*CustomSlug) OpenAPISchema(version string) *openapi.Schema { @@ -135,6 +172,89 @@ func TestConverter_SchemaExposer(t *testing.T) { ) } +func TestConverter_TextMarshaler(t *testing.T) { + r := reflect.NewReflector(&openapi.Config{OpenAPIVersion: openapi.Version312}) + + t.Run("direct type produces string schema", func(t *testing.T) { + s, err := r.SchemaForType(std_reflect.TypeFor[textStatus](), reflect.SchemaInline, nil) + require.NoError(t, err) + assert.Equal(t, "string", s.Type) + }) + + t.Run("pointer type preserves nullable", func(t *testing.T) { + s, err := r.SchemaForType(std_reflect.TypeFor[*textStatus](), reflect.SchemaInline, nil) + require.NoError(t, err) + assert.Equal(t, "string", s.Type.([]string)[0]) + assert.Equal(t, "null", s.Type.([]string)[1]) + }) + + t.Run("not a component type", func(t *testing.T) { + assert.False(t, reflect.IsComponentType(std_reflect.TypeFor[textStatus]())) + }) + + t.Run("json.Marshaler takes precedence — not reflected as string", func(t *testing.T) { + // textStatusJSONOverride implements TextMarshaler+TextUnmarshaler AND json.Marshaler. + // json.Marshaler wins → falls through to normal kindSwitch → integer. + s, err := r.SchemaForType(std_reflect.TypeFor[textStatusJSONOverride](), reflect.SchemaInline, nil) + require.NoError(t, err) + assert.Equal(t, "integer", s.Type, "must not receive string/text-marshaler treatment") + }) +} + +func TestConverter_EmbedRef(t *testing.T) { + t.Run("refer tag produces allOf ref, fields not inlined", func(t *testing.T) { + r := reflect.NewReflector(&openapi.Config{OpenAPIVersion: openapi.Version312}) + schema, err := r.StructSchema(std_reflect.TypeFor[embedRefViaTag](), "json", false, reflect.SchemaUseComponent) + require.NoError(t, err) + + require.Len(t, schema.AllOf, 1, "embedded type must appear in allOf") + assert.Equal(t, "#/components/schemas/embedBase", schema.AllOf[0].Ref) + + _, hasBaseField := schema.Properties["base_field"] + assert.False(t, hasBaseField, "base_field must not be inlined into parent properties") + + _, hasName := schema.Properties["name"] + assert.True(t, hasName, "own fields must still be present") + }) + + t.Run("EmbedReferencer interface produces allOf ref", func(t *testing.T) { + r := reflect.NewReflector(&openapi.Config{OpenAPIVersion: openapi.Version312}) + schema, err := r.StructSchema( + std_reflect.TypeFor[embedRefViaInterface](), + "json", + false, + reflect.SchemaUseComponent, + ) + require.NoError(t, err) + + require.Len(t, schema.AllOf, 1) + assert.Equal(t, "#/components/schemas/embedBaseViaInterface", schema.AllOf[0].Ref) + + _, hasOther := schema.Properties["other_field"] + assert.False(t, hasOther, "interface-embedded fields must not be inlined") + + _, hasID := schema.Properties["id"] + assert.True(t, hasID) + }) + + t.Run("plain embed without refer tag still inlines fields", func(t *testing.T) { + type plainBase struct { + BaseVal string `json:"base_val"` + } + type plainParent struct { + plainBase + + Extra string `json:"extra"` + } + r := reflect.NewReflector(&openapi.Config{OpenAPIVersion: openapi.Version312}) + schema, err := r.StructSchema(std_reflect.TypeFor[plainParent](), "json", false, reflect.SchemaInline) + require.NoError(t, err) + assert.Empty(t, schema.AllOf, "no allOf for plain embed") + _, hasBase := schema.Properties["base_val"] + assert.True(t, hasBase, "plain embed fields must be inlined") + }) +} + func TestConverter_SchemaForType_Branches(t *testing.T) { t.Run("primitive and collection kinds", func(t *testing.T) { r := reflect.NewReflector(&openapi.Config{OpenAPIVersion: openapi.Version312}) diff --git a/internal/reflect/reflector.go b/internal/reflect/reflector.go index 4c530b5..f989b95 100644 --- a/internal/reflect/reflector.go +++ b/internal/reflect/reflector.go @@ -251,6 +251,7 @@ func (r *Reflector) RefSchema(t reflect.Type) (*openapi.Schema, error) { r.Components[name].Type = built.Type r.Components[name].Properties = built.Properties r.Components[name].Required = built.Required + r.Components[name].AllOf = built.AllOf if interceptSchema != nil { postParams := openapi.InterceptSchemaParams{Type: t, Schema: r.Components[name], Processed: true} r.Config.Logger.Debug("interceptSchema: post-build called", "type", t.String(), "component", name) @@ -272,6 +273,22 @@ func (r *Reflector) StructSchema( mode SchemaMode, ) (*openapi.Schema, error) { schema := &openapi.Schema{Type: "object", Properties: map[string]*openapi.Schema{}} + // Pre-scan: collect embedded types opted into allOf $ref (via refer:"true" tag or EmbedReferencer). + for i := range t.NumField() { + field := t.Field(i) + if !field.Anonymous || TagName(field, "json") != "" { + continue + } + embType := IndirectType(field.Type) + if embType == nil || embType.Kind() != reflect.Struct || !isEmbedRef(field) { + continue + } + ref, err := r.RefSchema(embType) + if err != nil { + return nil, err + } + schema.AllOf = append(schema.AllOf, ref) + } interceptProp := r.interceptPropFn() parentType := t var firstErr error diff --git a/internal/reflect/utils.go b/internal/reflect/utils.go index ff185fd..295e333 100644 --- a/internal/reflect/utils.go +++ b/internal/reflect/utils.go @@ -90,6 +90,19 @@ func IndirectType(t reflect.Type) reflect.Type { return t } +var typeOfEmbedReferencer = reflect.TypeFor[openapi.EmbedReferencer]() + +func isEmbedRef(field reflect.StructField) bool { + if field.Tag.Get("refer") == "true" { + return true + } + t := IndirectType(field.Type) + if t == nil { + return false + } + return field.Type.Implements(typeOfEmbedReferencer) || reflect.PointerTo(t).Implements(typeOfEmbedReferencer) +} + func ForEachField(t reflect.Type, fn func(reflect.StructField)) { for i := range t.NumField() { field := t.Field(i) @@ -97,7 +110,9 @@ func ForEachField(t reflect.Type, fn func(reflect.StructField)) { continue } if field.Anonymous && IndirectType(field.Type).Kind() == reflect.Struct && TagName(field, "json") == "" { - ForEachField(IndirectType(field.Type), fn) + if !isEmbedRef(field) { + ForEachField(IndirectType(field.Type), fn) + } continue } fn(field) diff --git a/openapi/config.go b/openapi/config.go index 4adcd8d..f13628b 100644 --- a/openapi/config.go +++ b/openapi/config.go @@ -12,6 +12,14 @@ import ( // ErrSkipProperty can be returned from InterceptPropFunc to skip adding the property to the schema. var ErrSkipProperty = errors.New("skip property") +// EmbedReferencer can be implemented by an embedded struct type to opt into $ref-based embedding. +// When a struct embeds a type that implements EmbedReferencer (or is tagged `refer:"true"`), +// the embedded type is registered as a component schema and referenced via allOf instead of +// having its fields inlined into the parent schema. +type EmbedReferencer interface { + ReferEmbedded() +} + // InterceptPropParams defines parameters passed to InterceptPropFunc. // Called twice per field: before schema generation (Processed=false) and after (Processed=true). type InterceptPropParams struct { diff --git a/testdata/embed_ref.v30.yaml b/testdata/embed_ref.v30.yaml new file mode 100644 index 0000000..c77497b --- /dev/null +++ b/testdata/embed_ref.v30.yaml @@ -0,0 +1,64 @@ +openapi: 3.0.4 +info: + title: EmbedRef API + version: 1.0.0 +paths: + /refer-interface: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EmbedRefMixedRequest' + responses: + '204': + description: No Content + /refer-tag: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EmbedRefRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/EmbedRefBase' +components: + schemas: + EmbedRefBase: + type: object + properties: + id: + type: integer + format: int32 + role: + type: string + EmbedRefBaseViaInterface: + type: object + properties: + tag: + type: string + EmbedRefMixedRequest: + type: object + properties: + code: + type: string + value: + type: integer + format: int32 + allOf: + - $ref: '#/components/schemas/EmbedRefBaseViaInterface' + EmbedRefRequest: + type: object + properties: + age: + type: integer + format: int32 + name: + type: string + allOf: + - $ref: '#/components/schemas/EmbedRefBase' diff --git a/testdata/embed_ref.v31.yaml b/testdata/embed_ref.v31.yaml new file mode 100644 index 0000000..d81a744 --- /dev/null +++ b/testdata/embed_ref.v31.yaml @@ -0,0 +1,64 @@ +openapi: 3.1.2 +info: + title: EmbedRef API + version: 1.0.0 +paths: + /refer-interface: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EmbedRefMixedRequest' + responses: + '204': + description: No Content + /refer-tag: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EmbedRefRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/EmbedRefBase' +components: + schemas: + EmbedRefBase: + type: object + properties: + id: + type: integer + format: int32 + role: + type: string + EmbedRefBaseViaInterface: + type: object + properties: + tag: + type: string + EmbedRefMixedRequest: + type: object + properties: + code: + type: string + value: + type: integer + format: int32 + allOf: + - $ref: '#/components/schemas/EmbedRefBaseViaInterface' + EmbedRefRequest: + type: object + properties: + age: + type: integer + format: int32 + name: + type: string + allOf: + - $ref: '#/components/schemas/EmbedRefBase' diff --git a/testdata/embed_ref.v32.yaml b/testdata/embed_ref.v32.yaml new file mode 100644 index 0000000..49ef88e --- /dev/null +++ b/testdata/embed_ref.v32.yaml @@ -0,0 +1,64 @@ +openapi: 3.2.0 +info: + title: EmbedRef API + version: 1.0.0 +paths: + /refer-interface: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EmbedRefMixedRequest' + responses: + '204': + description: No Content + /refer-tag: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EmbedRefRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/EmbedRefBase' +components: + schemas: + EmbedRefBase: + type: object + properties: + id: + type: integer + format: int32 + role: + type: string + EmbedRefBaseViaInterface: + type: object + properties: + tag: + type: string + EmbedRefMixedRequest: + type: object + properties: + code: + type: string + value: + type: integer + format: int32 + allOf: + - $ref: '#/components/schemas/EmbedRefBaseViaInterface' + EmbedRefRequest: + type: object + properties: + age: + type: integer + format: int32 + name: + type: string + allOf: + - $ref: '#/components/schemas/EmbedRefBase' From f4d1d47d71efc05ecc011751dfdbaef621c36f10 Mon Sep 17 00:00:00 2001 From: Ahmad Faiz Kamaludin Date: Tue, 12 May 2026 07:25:30 +0700 Subject: [PATCH 5/8] feat: extend ParameterTagMapping to override form and json body tags Add ParameterInBody and ParameterInForm constants. When set in ParameterTagMapping, they override the struct tag used to name JSON body fields (default "json") and form body fields (default "form"). ParameterField skips these two keys so body-tagged fields are not misidentified as HTTP parameters. --- CHANGELOG.md | 1 + README.md | 2 +- internal/reflect/reflector.go | 6 +++- internal/reflect/reflector_test.go | 55 ++++++++++++++++++++++++++++++ internal/reflect/utils.go | 21 ++++++++++++ openapi/path.go | 7 ++++ 6 files changed, 90 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91860aa..79ee6f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Automatic content-type detection from struct type when not explicitly set. - `encoding.TextMarshaler`/`TextUnmarshaler` support: types implementing both interfaces (without `json.Marshaler`) are reflected as `type: string`. - `EmbedReferencer` interface and `refer:"true"` struct tag: embedded structs opt into `allOf $ref` instead of field inlining. +- `ParameterTagMapping` now accepts `openapi.ParameterInBody` and `openapi.ParameterInForm` to override the struct tag used for JSON and form request body field names (defaults: `json` and `form`). ### Fixed - `uint8`/`uint16` reflected as `int32` format; `uint`/`uint32`/`uint64`/`uintptr` reflected as `int64` format. diff --git a/README.md b/README.md index bb4330b..c382893 100644 --- a/README.md +++ b/README.md @@ -524,7 +524,7 @@ r := spec.NewRouter( | `InlineRefs(inline...)` | Inline schemas instead of using component references for named structs. | | `StripDefNamePrefix(prefixes...)` | Strip prefixes from generated component names. | | `TypeMapping(src, dst)` | Reflect `src` as if it were `dst`. | -| `ParameterTagMapping(in, sourceTag)` | Add a custom tag for a parameter location while keeping the default tag. | +| `ParameterTagMapping(in, sourceTag)` | Override the struct tag for a parameter location or body. Use `openapi.ParameterInBody` to replace `json` tag, `openapi.ParameterInForm` to replace `form` tag. | | `InterceptDefName(fn)` | Customize schema component names. | | `InterceptSchema(fn)` | Hook called before and after each type is reflected. Pre-call (`Processed=false`): return `stop=true` to override schema entirely. Post-call (`Processed=true`): modify the built schema. | | `InterceptProp(fn)` | Hook called before and after each struct field is reflected. Return `openapi.ErrSkipProperty` to exclude the field. | diff --git a/internal/reflect/reflector.go b/internal/reflect/reflector.go index f989b95..3574fa8 100644 --- a/internal/reflect/reflector.go +++ b/internal/reflect/reflector.go @@ -71,7 +71,7 @@ func (r *Reflector) RequestParts( } var params []*openapi.Parameter - bodyTag := BodyNameTag(ct) + bodyTag := r.bodyNameTag(ct) hasBody := false hasParam := false ForEachField(t, func(field reflect.StructField) { @@ -119,6 +119,10 @@ func (r *Reflector) ParameterField(field reflect.StructField) (string, string, b custom := map[openapi.ParameterIn]string{} if r.Config.ReflectorConfig != nil { for in, tag := range r.Config.ReflectorConfig.ParameterTagMapping { + // ParameterInBody and ParameterInForm override body tags, not parameter locations. + if in == openapi.ParameterInBody || in == openapi.ParameterInForm { + continue + } custom[in] = tag } } diff --git a/internal/reflect/reflector_test.go b/internal/reflect/reflector_test.go index 3cd51e1..35ef53d 100644 --- a/internal/reflect/reflector_test.go +++ b/internal/reflect/reflector_test.go @@ -183,6 +183,61 @@ func TestReflector_ParameterField_CustomMappingKeepsDefaultTag(t *testing.T) { assert.True(t, params[0].Required) } +func TestReflector_BodyTagMapping(t *testing.T) { + t.Run("ParameterInBody overrides json tag", func(t *testing.T) { + cfg := option.WithOpenAPIConfig( + option.WithReflectorConfig(option.ParameterTagMapping(openapi.ParameterInBody, "api")), + ) + r := reflect.NewReflector(cfg) + + type Req struct { + ID string `path:"id"` + Name string `api:"name"` + } + + params, body, err := r.RequestParts(Req{}, "application/json") + require.NoError(t, err) + require.Len(t, params, 1) + require.NotNil(t, body) + assert.Contains(t, body.Properties, "name") + assert.NotContains(t, body.Properties, "Name") + }) + + t.Run("ParameterInForm overrides form tag", func(t *testing.T) { + cfg := option.WithOpenAPIConfig( + option.WithReflectorConfig(option.ParameterTagMapping(openapi.ParameterInForm, "api")), + ) + r := reflect.NewReflector(cfg) + + type Req struct { + ID string `path:"id"` + Email string `api:"email"` + } + + params, body, err := r.RequestParts(Req{}, "application/x-www-form-urlencoded") + require.NoError(t, err) + require.Len(t, params, 1) + require.NotNil(t, body) + assert.Contains(t, body.Properties, "email") + }) + + t.Run("default body tags unchanged when no mapping", func(t *testing.T) { + cfg := option.WithOpenAPIConfig() + r := reflect.NewReflector(cfg) + + type Req struct { + ID string `path:"id"` + Name string `json:"name"` + } + + params, body, err := r.RequestParts(Req{}, "application/json") + require.NoError(t, err) + require.Len(t, params, 1) + require.NotNil(t, body) + assert.Contains(t, body.Properties, "name") + }) +} + func TestReflector_RequestPartsAndStructSchemaBranches(t *testing.T) { cfg := option.WithOpenAPIConfig() r := reflect.NewReflector(cfg) diff --git a/internal/reflect/utils.go b/internal/reflect/utils.go index 295e333..6c9358c 100644 --- a/internal/reflect/utils.go +++ b/internal/reflect/utils.go @@ -69,6 +69,27 @@ func (r *Reflector) InlineRefs() bool { return r.Config.ReflectorConfig != nil && r.Config.ReflectorConfig.InlineRefs } +// bodyNameTag returns the struct tag used to name body fields for the given content type. +// Defaults to "json" for JSON bodies and "form" for form bodies. +// Can be overridden via ParameterTagMapping with openapi.ParameterInBody or openapi.ParameterInForm. +func (r *Reflector) bodyNameTag(contentType string) string { + base := BodyNameTag(contentType) + if r.Config == nil || r.Config.ReflectorConfig == nil || r.Config.ReflectorConfig.ParameterTagMapping == nil { + return base + } + var key openapi.ParameterIn + switch base { + case "json": + key = openapi.ParameterInBody + case "form": + key = openapi.ParameterInForm + } + if tag, ok := r.Config.ReflectorConfig.ParameterTagMapping[key]; ok && tag != "" { + return tag + } + return base +} + func (r *Reflector) interceptPropFn() openapi.InterceptPropFunc { if r.Config == nil || r.Config.ReflectorConfig == nil { return nil diff --git a/openapi/path.go b/openapi/path.go index 915d225..367a886 100644 --- a/openapi/path.go +++ b/openapi/path.go @@ -56,4 +56,11 @@ const ( ParameterInHeader ParameterIn = "header" // ParameterInCookie indicates a cookie parameter. ParameterInCookie ParameterIn = "cookie" + + // ParameterInBody is used with ParameterTagMapping to override the struct tag used for JSON + // request body field names. Defaults to "json". + ParameterInBody ParameterIn = "body" + // ParameterInForm is used with ParameterTagMapping to override the struct tag used for form + // (application/x-www-form-urlencoded and multipart/form-data) request body field names. Defaults to "form". + ParameterInForm ParameterIn = "form" ) From 22042fa0976aaea38283065fc4e63ff00c1957b8 Mon Sep 17 00:00:00 2001 From: Ahmad Faiz Kamaludin Date: Tue, 12 May 2026 08:19:25 +0700 Subject: [PATCH 6/8] refactor(reflect)!: always prefix component names with package name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, def names were unqualified for types in the caller package (detected via runtime stack walking), and prefixed only for external packages. This caused cross-package name collisions and relied on fragile runtime introspection. Now all named types are prefixed with their Go package name, matching openapi-go / jsonschema-go behavior: - models.User → ModelsUser - dto.Pet → DtoPet Multi-segment package names are sanitized to UpperCamel (spec_test → SpecTest). Unexported type names are title-cased on join. BREAKING CHANGE: generated $ref component schema names gain a package prefix. Existing OpenAPI documents will have different schema keys. Use InterceptDefName or StripDefNamePrefix to strip the prefix if the old names are required. Removed: DefNameCallerPkg field on ReflectorConfig, ensureCallerPkgForDefName, callerPackagePath, packagePathFromFunc. --- CHANGELOG.md | 5 +++ README.md | 6 +++ internal/reflect/converter_test.go | 14 +++---- internal/reflect/reflector_test.go | 24 ++++++------ internal/reflect/utils.go | 32 +++++++++------ internal/reflect/utils_test.go | 17 ++++---- openapi/config.go | 1 - petstore_test.go | 3 +- router.go | 45 ---------------------- router_internal_test.go | 24 ------------ router_test.go | 8 ++-- testdata/anonymous_structs.v30.yaml | 4 +- testdata/anonymous_structs.v31.yaml | 4 +- testdata/anonymous_structs.v32.yaml | 4 +- testdata/basic_data_types.v30.yaml | 12 +++--- testdata/basic_data_types.v31.yaml | 12 +++--- testdata/basic_data_types.v32.yaml | 12 +++--- testdata/compatibility_extensions.v31.yaml | 4 +- testdata/complex_types.v30.yaml | 8 ++-- testdata/complex_types.v31.yaml | 8 ++-- testdata/complex_types.v32.yaml | 8 ++-- testdata/composition.v30.yaml | 4 +- testdata/composition.v31.yaml | 4 +- testdata/composition.v32.yaml | 4 +- testdata/custom_path_parser.v30.yaml | 4 +- testdata/custom_path_parser.v31.yaml | 4 +- testdata/custom_path_parser.v32.yaml | 4 +- testdata/embed_ref.v30.yaml | 18 ++++----- testdata/embed_ref.v31.yaml | 18 ++++----- testdata/embed_ref.v32.yaml | 18 ++++----- testdata/generics.v30.yaml | 16 ++++---- testdata/generics.v31.yaml | 16 ++++---- testdata/generics.v32.yaml | 16 ++++---- testdata/multipart_binary.v30.yaml | 4 +- testdata/multipart_binary.v31.yaml | 4 +- testdata/multipart_binary.v32.yaml | 4 +- testdata/multiple_content_types.v30.yaml | 12 +++--- testdata/multiple_content_types.v31.yaml | 12 +++--- testdata/multiple_content_types.v32.yaml | 12 +++--- testdata/nested_structures.v30.yaml | 4 +- testdata/nested_structures.v31.yaml | 4 +- testdata/nested_structures.v32.yaml | 4 +- testdata/openapi_320_features.v32.yaml | 6 +-- testdata/openapi_320_operations.v32.yaml | 4 +- testdata/path_parameters.v30.yaml | 4 +- testdata/path_parameters.v31.yaml | 4 +- testdata/path_parameters.v32.yaml | 4 +- testdata/request_response.v30.yaml | 8 ++-- testdata/request_response.v31.yaml | 8 ++-- testdata/request_response.v32.yaml | 8 ++-- testdata/security.v30.yaml | 4 +- testdata/security.v31.yaml | 4 +- testdata/security.v32.yaml | 4 +- 53 files changed, 224 insertions(+), 275 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79ee6f9..cc8d398 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `EmbedReferencer` interface and `refer:"true"` struct tag: embedded structs opt into `allOf $ref` instead of field inlining. - `ParameterTagMapping` now accepts `openapi.ParameterInBody` and `openapi.ParameterInForm` to override the struct tag used for JSON and form request body field names (defaults: `json` and `form`). +### Changed +- Schema component names now always include the Go package name as a prefix (e.g., `models.User` → `ModelsUser`). This eliminates cross-package naming collisions without requiring caller-package detection. Use `InterceptDefName` or `StripDefNamePrefix` to remove the prefix if desired. +- `DefNameCallerPkg` field removed from `ReflectorConfig`; the caller-package detection mechanism in `NewGenerator` is removed. +- Package name sanitization handles multi-segment names (e.g., `spec_test` → `SpecTest`); unexported type names are title-cased when a package prefix is prepended. + ### Fixed - `uint8`/`uint16` reflected as `int32` format; `uint`/`uint32`/`uint64`/`uintptr` reflected as `int64` format. - `InterceptSchema`/`InterceptProp` hook error propagation and correctness. diff --git a/README.md b/README.md index c382893..ca10029 100644 --- a/README.md +++ b/README.md @@ -618,3 +618,9 @@ Issues and pull requests are welcome. Please check existing issues and discussio ## License [MIT](LICENSE) + +--- + +## Acknowledgements + +- [swaggest/openapi-go](https://github.com/swaggest/openapi-go) and [swaggest/jsonschema-go](https://github.com/swaggest/jsonschema-go) — parts of this library's reflection design are inspired by their work. diff --git a/internal/reflect/converter_test.go b/internal/reflect/converter_test.go index f30d86f..557f0aa 100644 --- a/internal/reflect/converter_test.go +++ b/internal/reflect/converter_test.go @@ -128,7 +128,7 @@ func TestConverter_CustomSchemaExposer(t *testing.T) { raw, err := r.GenerateSchema("json") require.NoError(t, err) - id := generatedComponentProperty(t, raw, "CustomSchemaPayload", "id") + id := generatedComponentProperty(t, raw, "ReflectTestCustomSchemaPayload", "id") assert.Equal(t, "slug", id["format"]) assert.Equal(t, "Stable identifier", id["description"]) } @@ -141,7 +141,7 @@ func TestConverter_Nullable(t *testing.T) { raw, err := r.GenerateSchema("json") require.NoError(t, err) - owner := generatedComponentProperty(t, raw, "ReflectionVersionPayload", "owner") + owner := generatedComponentProperty(t, raw, "ReflectTestReflectionVersionPayload", "owner") assert.NotContains(t, owner, "$ref", "nullable component refs must not emit $ref siblings in 3.0") assert.Equal(t, true, owner["nullable"]) }) @@ -153,7 +153,7 @@ func TestConverter_Nullable(t *testing.T) { raw, err := r.GenerateSchema("json") require.NoError(t, err) - name := generatedComponentProperty(t, raw, "ReflectionVersionPayload312", "name") + name := generatedComponentProperty(t, raw, "ReflectTestReflectionVersionPayload312", "name") typ := name["type"].([]any) assert.Equal(t, []any{"string", "null"}, typ) }) @@ -208,7 +208,7 @@ func TestConverter_EmbedRef(t *testing.T) { require.NoError(t, err) require.Len(t, schema.AllOf, 1, "embedded type must appear in allOf") - assert.Equal(t, "#/components/schemas/embedBase", schema.AllOf[0].Ref) + assert.Equal(t, "#/components/schemas/ReflectTestEmbedBase", schema.AllOf[0].Ref) _, hasBaseField := schema.Properties["base_field"] assert.False(t, hasBaseField, "base_field must not be inlined into parent properties") @@ -228,7 +228,7 @@ func TestConverter_EmbedRef(t *testing.T) { require.NoError(t, err) require.Len(t, schema.AllOf, 1) - assert.Equal(t, "#/components/schemas/embedBaseViaInterface", schema.AllOf[0].Ref) + assert.Equal(t, "#/components/schemas/ReflectTestEmbedBaseViaInterface", schema.AllOf[0].Ref) _, hasOther := schema.Properties["other_field"] assert.False(t, hasOther, "interface-embedded fields must not be inlined") @@ -386,13 +386,13 @@ func TestConverter_SchemaForType_Branches(t *testing.T) { require.NoError(t, err) assert.True(t, s304.Nullable) require.Len(t, s304.AllOf, 1) - assert.Equal(t, "#/components/schemas/User", s304.AllOf[0].Ref) + assert.Equal(t, "#/components/schemas/ReflectTestUser", s304.AllOf[0].Ref) r312 := reflect.NewReflector(&openapi.Config{OpenAPIVersion: openapi.Version312}) s312, err := r312.SchemaForType(std_reflect.TypeFor[*User](), reflect.SchemaUseComponent, nil) require.NoError(t, err) require.Len(t, s312.AnyOf, 2) - assert.Equal(t, "#/components/schemas/User", s312.AnyOf[0].Ref) + assert.Equal(t, "#/components/schemas/ReflectTestUser", s312.AnyOf[0].Ref) assert.Equal(t, "null", s312.AnyOf[1].Type) }) } diff --git a/internal/reflect/reflector_test.go b/internal/reflect/reflector_test.go index 35ef53d..5144f3f 100644 --- a/internal/reflect/reflector_test.go +++ b/internal/reflect/reflector_test.go @@ -116,7 +116,7 @@ func TestReflector_Config(t *testing.T) { assert.Contains(t, doc.Components.Schemas, "Collision2") }) - t.Run("DefaultDefNameUsesPkgPrefixExceptCallerPackage", func(t *testing.T) { + t.Run("DefaultDefNameUsesPkgPrefixForAllTypes", func(t *testing.T) { r := spec.NewRouter() type SamePkgModel struct{ Foo string } @@ -127,7 +127,7 @@ func TestReflector_Config(t *testing.T) { require.NoError(t, err) doc := r.Document() - assert.Contains(t, doc.Components.Schemas, "SamePkgModel") + assert.Contains(t, doc.Components.Schemas, "ReflectTestSamePkgModel") assert.Contains(t, doc.Components.Schemas, "DtoPet") }) @@ -298,7 +298,7 @@ func TestReflector_RequestPartsAndStructSchemaBranches(t *testing.T) { require.NoError(t, err) assert.Nil(t, params) require.NotNil(t, body) - assert.Equal(t, "#/components/schemas/Dst", body.Ref) + assert.Equal(t, "#/components/schemas/ReflectTestDst", body.Ref) }) } @@ -541,8 +541,8 @@ func TestStructSchema_InterceptSchema(t *testing.T) { _, err := r.GenerateSchema("yaml") require.NoError(t, err) doc := r.Document() - require.Contains(t, doc.Components.Schemas, "Item") - assert.Equal(t, true, doc.Components.Schemas["Item"].Extensions["x-intercepted"]) + require.Contains(t, doc.Components.Schemas, "ReflectTestItem") + assert.Equal(t, true, doc.Components.Schemas["ReflectTestItem"].Extensions["x-intercepted"]) }) t.Run("PreHookStopOnComponentSkipsStructSchema", func(t *testing.T) { @@ -563,9 +563,9 @@ func TestStructSchema_InterceptSchema(t *testing.T) { _, err := r.GenerateSchema("yaml") require.NoError(t, err) doc := r.Document() - require.Contains(t, doc.Components.Schemas, "Skipped") - assert.Equal(t, "custom", doc.Components.Schemas["Skipped"].Description) - assert.Nil(t, doc.Components.Schemas["Skipped"].Properties) // StructSchema was skipped + require.Contains(t, doc.Components.Schemas, "ReflectTestSkipped") + assert.Equal(t, "custom", doc.Components.Schemas["ReflectTestSkipped"].Description) + assert.Nil(t, doc.Components.Schemas["ReflectTestSkipped"].Properties) // StructSchema was skipped }) t.Run("PreHookErrorPropagated", func(t *testing.T) { @@ -697,7 +697,7 @@ func TestStructSchema_InterceptSchema(t *testing.T) { // Second call must retry (not hit stale empty component from first call). schema, err := r.SchemaForType(std_reflect.TypeFor[Target](), reflect.SchemaUseComponent, nil) require.NoError(t, err) - assert.Equal(t, "#/components/schemas/Target", schema.Ref) + assert.Equal(t, "#/components/schemas/ReflectTestTarget", schema.Ref) assert.Equal(t, 2, calls) }) } @@ -721,9 +721,9 @@ func TestReflector_InterceptPropViaRouter(t *testing.T) { _, err := r.GenerateSchema("yaml") require.NoError(t, err) doc := r.Document() - require.Contains(t, doc.Components.Schemas, "Item") - assert.Contains(t, doc.Components.Schemas["Item"].Properties, "name") - assert.NotContains(t, doc.Components.Schemas["Item"].Properties, "hidden") + require.Contains(t, doc.Components.Schemas, "ReflectTestItem") + assert.Contains(t, doc.Components.Schemas["ReflectTestItem"].Properties, "name") + assert.NotContains(t, doc.Components.Schemas["ReflectTestItem"].Properties, "hidden") } func TestStructSchema_InterceptProp_NonSkipErrorPropagated(t *testing.T) { diff --git a/internal/reflect/utils.go b/internal/reflect/utils.go index 6c9358c..9d79393 100644 --- a/internal/reflect/utils.go +++ b/internal/reflect/utils.go @@ -17,7 +17,7 @@ func (r *Reflector) TypeName(t reflect.Type) string { return name } name := SanitizeTypeName(t.Name()) - name = sanitizeDefName(t, name, r.callerPkgPath()) + name = prefixWithPkg(t, name) for _, prefix := range r.StripPrefixes() { name = strings.TrimPrefix(name, prefix) } @@ -39,23 +39,33 @@ func (r *Reflector) TypeName(t reflect.Type) string { return name } -func sanitizeDefName(t reflect.Type, defaultDefName, callerPkgPath string) string { - if callerPkgPath == "" || defaultDefName == "" || t == nil || t.PkgPath() == "" || t.PkgPath() == callerPkgPath { - return defaultDefName +func prefixWithPkg(t reflect.Type, defName string) string { + if defName == "" || t == nil || t.PkgPath() == "" || t.PkgPath() == "main" { + return defName } pkgName := path.Base(t.PkgPath()) if pkgName == "" || pkgName == "main" { - return defaultDefName + return defName } - pkgName = strings.ToUpper(pkgName[:1]) + pkgName[1:] - return pkgName + defaultDefName + pkgName = sanitizePkgName(pkgName) + if pkgName == "" { + return defName + } + return pkgName + strings.ToUpper(defName[:1]) + defName[1:] } -func (r *Reflector) callerPkgPath() string { - if r.Config == nil || r.Config.ReflectorConfig == nil { - return "" +func sanitizePkgName(name string) string { + parts := strings.FieldsFunc(name, func(r rune) bool { + return r == '_' || r == '-' || r == '.' + }) + var b strings.Builder + for _, p := range parts { + if len(p) == 0 { + continue + } + b.WriteString(strings.ToUpper(p[:1]) + p[1:]) } - return r.Config.ReflectorConfig.DefNameCallerPkg + return b.String() } func (r *Reflector) StripPrefixes() []string { diff --git a/internal/reflect/utils_test.go b/internal/reflect/utils_test.go index 74db1f4..36655c7 100644 --- a/internal/reflect/utils_test.go +++ b/internal/reflect/utils_test.go @@ -71,28 +71,25 @@ func TestForEachField(t *testing.T) { } func TestInternalHelpers(t *testing.T) { - t.Run("sanitizeDefName", func(t *testing.T) { - assert.Equal(t, "Model", sanitizeDefName(nil, "Model", "github.com/oaswrap/spec")) - assert.Equal(t, "Model", sanitizeDefName(reflect.TypeFor[struct{}](), "Model", "github.com/oaswrap/spec")) - assert.Equal(t, "Model", sanitizeDefName(reflect.TypeFor[time.Time](), "Model", "")) - assert.Equal(t, "TimeModel", sanitizeDefName(reflect.TypeFor[time.Time](), "Model", "github.com/oaswrap/spec")) + t.Run("prefixWithPkg", func(t *testing.T) { + assert.Equal(t, "Model", prefixWithPkg(nil, "Model")) + assert.Equal(t, "Model", prefixWithPkg(reflect.TypeFor[struct{}](), "Model")) + assert.Equal(t, "TimeModel", prefixWithPkg(reflect.TypeFor[time.Time](), "Model")) + assert.Empty(t, prefixWithPkg(reflect.TypeFor[time.Time](), "")) }) t.Run("reflector helper accessors", func(t *testing.T) { r := &Reflector{} - assert.Empty(t, r.callerPkgPath()) assert.Nil(t, r.interceptPropFn()) assert.Nil(t, r.interceptSchemaFn()) cfg := &openapi.Config{ ReflectorConfig: &openapi.ReflectorConfig{ - DefNameCallerPkg: "github.com/oaswrap/spec", - InterceptProp: func(openapi.InterceptPropParams) error { return nil }, - InterceptSchema: func(openapi.InterceptSchemaParams) (bool, error) { return false, nil }, + InterceptProp: func(openapi.InterceptPropParams) error { return nil }, + InterceptSchema: func(openapi.InterceptSchemaParams) (bool, error) { return false, nil }, }, } r = &Reflector{Config: cfg} - assert.Equal(t, "github.com/oaswrap/spec", r.callerPkgPath()) assert.NotNil(t, r.interceptPropFn()) assert.NotNil(t, r.interceptSchemaFn()) }) diff --git a/openapi/config.go b/openapi/config.go index f13628b..7916d1f 100644 --- a/openapi/config.go +++ b/openapi/config.go @@ -117,7 +117,6 @@ type ReflectorConfig struct { InterceptDefName func(t reflect.Type, defaultDefName string) string InterceptProp InterceptPropFunc InterceptSchema InterceptSchemaFunc - DefNameCallerPkg string TypeMappings []TypeMapping ParameterTagMapping map[ParameterIn]string } diff --git a/petstore_test.go b/petstore_test.go index 1b3eed4..211cc02 100644 --- a/petstore_test.go +++ b/petstore_test.go @@ -142,7 +142,8 @@ func newPetstoreRouter(opts ...option.OpenAPIOption) spec.Generator { option.WithSecurity("api_key", option.SecurityAPIKey("api_key", "header")), option.WithReflectorConfig( option.InterceptDefName(func(_ reflect.Type, defaultName string) string { - return strings.TrimPrefix(defaultName, "Petstore") + name := strings.TrimPrefix(defaultName, "SpecTest") + return strings.TrimPrefix(name, "Petstore") }), ), option.WithDocument(func(doc *openapi.Document) { diff --git a/router.go b/router.go index 0a29d8d..f183b56 100644 --- a/router.go +++ b/router.go @@ -6,7 +6,6 @@ import ( "fmt" "net/http" "os" - "runtime" "slices" "strings" "sync" @@ -79,7 +78,6 @@ func NewRouter(opts ...option.OpenAPIOption) Generator { // fmt.Println(string(schema)) func NewGenerator(opts ...option.OpenAPIOption) Generator { cfg := option.WithOpenAPIConfig(opts...) - ensureCallerPkgForDefName(cfg, callerPackagePath()) g := &generator{ cfg: cfg, } @@ -87,49 +85,6 @@ func NewGenerator(opts ...option.OpenAPIOption) Generator { return g } -func ensureCallerPkgForDefName(cfg *openapi.Config, callerPkgPath string) { - if cfg.ReflectorConfig == nil { - cfg.ReflectorConfig = &openapi.ReflectorConfig{} - } - cfg.ReflectorConfig.DefNameCallerPkg = callerPkgPath -} - -func callerPackagePath() string { - pcs := make([]uintptr, 16) - n := runtime.Callers(2, pcs) - frames := runtime.CallersFrames(pcs[:n]) - - for { - frame, more := frames.Next() - pkgPath := packagePathFromFunc(frame.Function) - if pkgPath != "" && pkgPath != "github.com/oaswrap/spec" { - return pkgPath - } - if !more { - break - } - } - - return "" -} - -func packagePathFromFunc(funcName string) string { - if funcName == "" { - return "" - } - idx := strings.LastIndex(funcName, "/") - if idx == -1 { - idx = 0 - } else { - idx++ - } - dot := strings.IndexByte(funcName[idx:], '.') - if dot == -1 { - return "" - } - return funcName[:idx+dot] -} - func (g *generator) Config() *openapi.Config { return g.cfg } diff --git a/router_internal_test.go b/router_internal_test.go index 618a71e..2ebf062 100644 --- a/router_internal_test.go +++ b/router_internal_test.go @@ -66,27 +66,3 @@ func TestRoutePathRespectsPrefixForNonWebhook(t *testing.T) { webhook.Path("user.created") assert.Equal(t, "user.created", webhook.path) } - -func TestPackagePathFromFunc(t *testing.T) { - tests := []struct { - name string - funcName string - want string - }{ - {name: "empty", funcName: "", want: ""}, - {name: "no dot", funcName: "github.com/oaswrap/spec", want: ""}, - {name: "full package function", funcName: "github.com/oaswrap/spec.NewRouter", want: "github.com/oaswrap/spec"}, - { - name: "method receiver", - funcName: "github.com/oaswrap/spec.(*generator).Add", - want: "github.com/oaswrap/spec", - }, - {name: "simple package function", funcName: "main.main", want: "main"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, packagePathFromFunc(tt.funcName)) - }) - } -} diff --git a/router_test.go b/router_test.go index d0ff79f..f3e096a 100644 --- a/router_test.go +++ b/router_test.go @@ -73,8 +73,8 @@ func TestRouter_GenerateSchema_DefaultVersion(t *testing.T) { } assert.NotContains(t, doc.Components.Schemas, "user", "component names should use exported Go type names") - assert.NotNil(t, doc.Components.Schemas["User"]) - assert.NotNil(t, doc.Components.Schemas["LoginRequest"]) + assert.NotNil(t, doc.Components.Schemas["SpecTestUser"]) + assert.NotNil(t, doc.Components.Schemas["SpecTestLoginRequest"]) assert.Equal(t, "http", doc.Components.SecuritySchemes["bearerAuth"].Type) } @@ -207,7 +207,7 @@ func TestSchemaSkipsJSONIgnoredFields(t *testing.T) { doc := r.Document() require.NoError(t, r.Validate()) - payload := doc.Components.Schemas["Payload"] + payload := doc.Components.Schemas["SpecTestPayload"] assert.NotNil(t, payload.Properties["public"]) assert.NotContains(t, payload.Properties, "Secret") assert.NotContains(t, payload.Properties, "secret") @@ -453,7 +453,7 @@ func TestRouter_EscapeHatches(t *testing.T) { } doc.Components.MediaTypes = map[string]*openapi.MediaType{ "json-seq": { - ItemSchema: &openapi.Schema{Ref: "#/components/schemas/User"}, + ItemSchema: &openapi.Schema{Ref: "#/components/schemas/SpecTestUser"}, ItemEncoding: &openapi.Encoding{ ContentType: "application/json", Extensions: map[string]any{"x-encoding": true}, diff --git a/testdata/anonymous_structs.v30.yaml b/testdata/anonymous_structs.v30.yaml index 0c36af1..b9408dc 100644 --- a/testdata/anonymous_structs.v30.yaml +++ b/testdata/anonymous_structs.v30.yaml @@ -9,13 +9,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AnonymousStructRequest' + $ref: '#/components/schemas/SpecTestAnonymousStructRequest' responses: '204': description: No Content components: schemas: - AnonymousStructRequest: + SpecTestAnonymousStructRequest: type: object properties: foo: diff --git a/testdata/anonymous_structs.v31.yaml b/testdata/anonymous_structs.v31.yaml index ae13ea0..c66b14d 100644 --- a/testdata/anonymous_structs.v31.yaml +++ b/testdata/anonymous_structs.v31.yaml @@ -9,13 +9,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AnonymousStructRequest' + $ref: '#/components/schemas/SpecTestAnonymousStructRequest' responses: '204': description: No Content components: schemas: - AnonymousStructRequest: + SpecTestAnonymousStructRequest: type: object properties: foo: diff --git a/testdata/anonymous_structs.v32.yaml b/testdata/anonymous_structs.v32.yaml index 2fa1c09..37be007 100644 --- a/testdata/anonymous_structs.v32.yaml +++ b/testdata/anonymous_structs.v32.yaml @@ -9,13 +9,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AnonymousStructRequest' + $ref: '#/components/schemas/SpecTestAnonymousStructRequest' responses: '204': description: No Content components: schemas: - AnonymousStructRequest: + SpecTestAnonymousStructRequest: type: object properties: foo: diff --git a/testdata/basic_data_types.v30.yaml b/testdata/basic_data_types.v30.yaml index e37f838..9d51c24 100644 --- a/testdata/basic_data_types.v30.yaml +++ b/testdata/basic_data_types.v30.yaml @@ -10,14 +10,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AllBasicDataTypes' + $ref: '#/components/schemas/SpecTestAllBasicDataTypes' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/AllBasicDataTypes' + $ref: '#/components/schemas/SpecTestAllBasicDataTypes' /basic-types-pointers: post: operationId: basicTypesPointers @@ -25,17 +25,17 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AllBasicDataTypesPointers' + $ref: '#/components/schemas/SpecTestAllBasicDataTypesPointers' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/AllBasicDataTypesPointers' + $ref: '#/components/schemas/SpecTestAllBasicDataTypesPointers' components: schemas: - AllBasicDataTypes: + SpecTestAllBasicDataTypes: type: object properties: bool: @@ -94,7 +94,7 @@ components: type: integer format: int64 minimum: 0.0 - AllBasicDataTypesPointers: + SpecTestAllBasicDataTypesPointers: type: object properties: bool: diff --git a/testdata/basic_data_types.v31.yaml b/testdata/basic_data_types.v31.yaml index bdca3fb..377878f 100644 --- a/testdata/basic_data_types.v31.yaml +++ b/testdata/basic_data_types.v31.yaml @@ -10,14 +10,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AllBasicDataTypes' + $ref: '#/components/schemas/SpecTestAllBasicDataTypes' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/AllBasicDataTypes' + $ref: '#/components/schemas/SpecTestAllBasicDataTypes' /basic-types-pointers: post: operationId: basicTypesPointers @@ -25,17 +25,17 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AllBasicDataTypesPointers' + $ref: '#/components/schemas/SpecTestAllBasicDataTypesPointers' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/AllBasicDataTypesPointers' + $ref: '#/components/schemas/SpecTestAllBasicDataTypesPointers' components: schemas: - AllBasicDataTypes: + SpecTestAllBasicDataTypes: type: object properties: bool: @@ -94,7 +94,7 @@ components: type: integer format: int64 minimum: 0.0 - AllBasicDataTypesPointers: + SpecTestAllBasicDataTypesPointers: type: object properties: bool: diff --git a/testdata/basic_data_types.v32.yaml b/testdata/basic_data_types.v32.yaml index 2bbcf95..dadb1a8 100644 --- a/testdata/basic_data_types.v32.yaml +++ b/testdata/basic_data_types.v32.yaml @@ -10,14 +10,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AllBasicDataTypes' + $ref: '#/components/schemas/SpecTestAllBasicDataTypes' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/AllBasicDataTypes' + $ref: '#/components/schemas/SpecTestAllBasicDataTypes' /basic-types-pointers: post: operationId: basicTypesPointers @@ -25,17 +25,17 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AllBasicDataTypesPointers' + $ref: '#/components/schemas/SpecTestAllBasicDataTypesPointers' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/AllBasicDataTypesPointers' + $ref: '#/components/schemas/SpecTestAllBasicDataTypesPointers' components: schemas: - AllBasicDataTypes: + SpecTestAllBasicDataTypes: type: object properties: bool: @@ -94,7 +94,7 @@ components: type: integer format: int64 minimum: 0.0 - AllBasicDataTypesPointers: + SpecTestAllBasicDataTypesPointers: type: object properties: bool: diff --git a/testdata/compatibility_extensions.v31.yaml b/testdata/compatibility_extensions.v31.yaml index 2b03a58..ae3e2c6 100644 --- a/testdata/compatibility_extensions.v31.yaml +++ b/testdata/compatibility_extensions.v31.yaml @@ -19,7 +19,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/SpecTestUser' x-operation: ok webhooks: user.created: @@ -29,7 +29,7 @@ webhooks: description: Accepted components: schemas: - User: + SpecTestUser: type: object required: - id diff --git a/testdata/complex_types.v30.yaml b/testdata/complex_types.v30.yaml index abcfb46..9bad1a3 100644 --- a/testdata/complex_types.v30.yaml +++ b/testdata/complex_types.v30.yaml @@ -10,17 +10,17 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ComplexRequest' + $ref: '#/components/schemas/SpecTestComplexRequest' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/ComplexResponse' + $ref: '#/components/schemas/SpecTestComplexResponse' components: schemas: - ComplexRequest: + SpecTestComplexRequest: type: object required: - string @@ -69,7 +69,7 @@ components: maxLength: 10 minLength: 1 pattern: ^[a-z]+$ - ComplexResponse: + SpecTestComplexResponse: type: object properties: data: diff --git a/testdata/complex_types.v31.yaml b/testdata/complex_types.v31.yaml index b126e18..7e4e033 100644 --- a/testdata/complex_types.v31.yaml +++ b/testdata/complex_types.v31.yaml @@ -10,17 +10,17 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ComplexRequest' + $ref: '#/components/schemas/SpecTestComplexRequest' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/ComplexResponse' + $ref: '#/components/schemas/SpecTestComplexResponse' components: schemas: - ComplexRequest: + SpecTestComplexRequest: type: object required: - string @@ -71,7 +71,7 @@ components: maxLength: 10 minLength: 1 pattern: ^[a-z]+$ - ComplexResponse: + SpecTestComplexResponse: type: object properties: data: diff --git a/testdata/complex_types.v32.yaml b/testdata/complex_types.v32.yaml index 8318466..341c00d 100644 --- a/testdata/complex_types.v32.yaml +++ b/testdata/complex_types.v32.yaml @@ -10,17 +10,17 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ComplexRequest' + $ref: '#/components/schemas/SpecTestComplexRequest' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/ComplexResponse' + $ref: '#/components/schemas/SpecTestComplexResponse' components: schemas: - ComplexRequest: + SpecTestComplexRequest: type: object required: - string @@ -71,7 +71,7 @@ components: maxLength: 10 minLength: 1 pattern: ^[a-z]+$ - ComplexResponse: + SpecTestComplexResponse: type: object properties: data: diff --git a/testdata/composition.v30.yaml b/testdata/composition.v30.yaml index 0093aa8..40ea2cb 100644 --- a/testdata/composition.v30.yaml +++ b/testdata/composition.v30.yaml @@ -9,7 +9,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/OneOfRequest' + $ref: '#/components/schemas/SpecTestOneOfRequest' responses: '200': description: OK @@ -21,7 +21,7 @@ paths: - type: string components: schemas: - OneOfRequest: + SpecTestOneOfRequest: type: object properties: value: {} diff --git a/testdata/composition.v31.yaml b/testdata/composition.v31.yaml index 59643d9..14d0b6a 100644 --- a/testdata/composition.v31.yaml +++ b/testdata/composition.v31.yaml @@ -9,7 +9,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/OneOfRequest' + $ref: '#/components/schemas/SpecTestOneOfRequest' responses: '200': description: OK @@ -21,7 +21,7 @@ paths: - type: string components: schemas: - OneOfRequest: + SpecTestOneOfRequest: type: object properties: value: {} diff --git a/testdata/composition.v32.yaml b/testdata/composition.v32.yaml index 7570ae9..4b38451 100644 --- a/testdata/composition.v32.yaml +++ b/testdata/composition.v32.yaml @@ -9,7 +9,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/OneOfRequest' + $ref: '#/components/schemas/SpecTestOneOfRequest' responses: '200': description: OK @@ -21,7 +21,7 @@ paths: - type: string components: schemas: - OneOfRequest: + SpecTestOneOfRequest: type: object properties: value: {} diff --git a/testdata/custom_path_parser.v30.yaml b/testdata/custom_path_parser.v30.yaml index 86c24f9..7d704a8 100644 --- a/testdata/custom_path_parser.v30.yaml +++ b/testdata/custom_path_parser.v30.yaml @@ -19,10 +19,10 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/SpecTestUser' components: schemas: - User: + SpecTestUser: type: object required: - id diff --git a/testdata/custom_path_parser.v31.yaml b/testdata/custom_path_parser.v31.yaml index 3ef9a73..c60971a 100644 --- a/testdata/custom_path_parser.v31.yaml +++ b/testdata/custom_path_parser.v31.yaml @@ -19,10 +19,10 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/SpecTestUser' components: schemas: - User: + SpecTestUser: type: object required: - id diff --git a/testdata/custom_path_parser.v32.yaml b/testdata/custom_path_parser.v32.yaml index 6972ddf..c916079 100644 --- a/testdata/custom_path_parser.v32.yaml +++ b/testdata/custom_path_parser.v32.yaml @@ -19,10 +19,10 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/SpecTestUser' components: schemas: - User: + SpecTestUser: type: object required: - id diff --git a/testdata/embed_ref.v30.yaml b/testdata/embed_ref.v30.yaml index c77497b..2b5686f 100644 --- a/testdata/embed_ref.v30.yaml +++ b/testdata/embed_ref.v30.yaml @@ -9,7 +9,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EmbedRefMixedRequest' + $ref: '#/components/schemas/SpecTestEmbedRefMixedRequest' responses: '204': description: No Content @@ -19,17 +19,17 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EmbedRefRequest' + $ref: '#/components/schemas/SpecTestEmbedRefRequest' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/EmbedRefBase' + $ref: '#/components/schemas/SpecTestEmbedRefBase' components: schemas: - EmbedRefBase: + SpecTestEmbedRefBase: type: object properties: id: @@ -37,12 +37,12 @@ components: format: int32 role: type: string - EmbedRefBaseViaInterface: + SpecTestEmbedRefBaseViaInterface: type: object properties: tag: type: string - EmbedRefMixedRequest: + SpecTestEmbedRefMixedRequest: type: object properties: code: @@ -51,8 +51,8 @@ components: type: integer format: int32 allOf: - - $ref: '#/components/schemas/EmbedRefBaseViaInterface' - EmbedRefRequest: + - $ref: '#/components/schemas/SpecTestEmbedRefBaseViaInterface' + SpecTestEmbedRefRequest: type: object properties: age: @@ -61,4 +61,4 @@ components: name: type: string allOf: - - $ref: '#/components/schemas/EmbedRefBase' + - $ref: '#/components/schemas/SpecTestEmbedRefBase' diff --git a/testdata/embed_ref.v31.yaml b/testdata/embed_ref.v31.yaml index d81a744..a6b715d 100644 --- a/testdata/embed_ref.v31.yaml +++ b/testdata/embed_ref.v31.yaml @@ -9,7 +9,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EmbedRefMixedRequest' + $ref: '#/components/schemas/SpecTestEmbedRefMixedRequest' responses: '204': description: No Content @@ -19,17 +19,17 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EmbedRefRequest' + $ref: '#/components/schemas/SpecTestEmbedRefRequest' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/EmbedRefBase' + $ref: '#/components/schemas/SpecTestEmbedRefBase' components: schemas: - EmbedRefBase: + SpecTestEmbedRefBase: type: object properties: id: @@ -37,12 +37,12 @@ components: format: int32 role: type: string - EmbedRefBaseViaInterface: + SpecTestEmbedRefBaseViaInterface: type: object properties: tag: type: string - EmbedRefMixedRequest: + SpecTestEmbedRefMixedRequest: type: object properties: code: @@ -51,8 +51,8 @@ components: type: integer format: int32 allOf: - - $ref: '#/components/schemas/EmbedRefBaseViaInterface' - EmbedRefRequest: + - $ref: '#/components/schemas/SpecTestEmbedRefBaseViaInterface' + SpecTestEmbedRefRequest: type: object properties: age: @@ -61,4 +61,4 @@ components: name: type: string allOf: - - $ref: '#/components/schemas/EmbedRefBase' + - $ref: '#/components/schemas/SpecTestEmbedRefBase' diff --git a/testdata/embed_ref.v32.yaml b/testdata/embed_ref.v32.yaml index 49ef88e..b56518f 100644 --- a/testdata/embed_ref.v32.yaml +++ b/testdata/embed_ref.v32.yaml @@ -9,7 +9,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EmbedRefMixedRequest' + $ref: '#/components/schemas/SpecTestEmbedRefMixedRequest' responses: '204': description: No Content @@ -19,17 +19,17 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EmbedRefRequest' + $ref: '#/components/schemas/SpecTestEmbedRefRequest' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/EmbedRefBase' + $ref: '#/components/schemas/SpecTestEmbedRefBase' components: schemas: - EmbedRefBase: + SpecTestEmbedRefBase: type: object properties: id: @@ -37,12 +37,12 @@ components: format: int32 role: type: string - EmbedRefBaseViaInterface: + SpecTestEmbedRefBaseViaInterface: type: object properties: tag: type: string - EmbedRefMixedRequest: + SpecTestEmbedRefMixedRequest: type: object properties: code: @@ -51,8 +51,8 @@ components: type: integer format: int32 allOf: - - $ref: '#/components/schemas/EmbedRefBaseViaInterface' - EmbedRefRequest: + - $ref: '#/components/schemas/SpecTestEmbedRefBaseViaInterface' + SpecTestEmbedRefRequest: type: object properties: age: @@ -61,4 +61,4 @@ components: name: type: string allOf: - - $ref: '#/components/schemas/EmbedRefBase' + - $ref: '#/components/schemas/SpecTestEmbedRefBase' diff --git a/testdata/generics.v30.yaml b/testdata/generics.v30.yaml index 8d4d678..ff30b13 100644 --- a/testdata/generics.v30.yaml +++ b/testdata/generics.v30.yaml @@ -11,7 +11,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ProfileResponse' + $ref: '#/components/schemas/SpecTestProfileResponse' /user: get: responses: @@ -20,7 +20,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/BaseResponseUser' + $ref: '#/components/schemas/SpecTestBaseResponseUser' /users: post: responses: @@ -29,10 +29,10 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/BaseResponseUserList' + $ref: '#/components/schemas/SpecTestBaseResponseUserList' components: schemas: - BaseResponseUser: + SpecTestBaseResponseUser: type: object properties: data: @@ -48,18 +48,18 @@ components: type: string success: type: boolean - BaseResponseUserList: + SpecTestBaseResponseUserList: type: object properties: data: type: array items: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/SpecTestUser' message: type: string success: type: boolean - ProfileResponse: + SpecTestProfileResponse: type: object properties: data: @@ -75,7 +75,7 @@ components: type: string success: type: boolean - User: + SpecTestUser: type: object required: - id diff --git a/testdata/generics.v31.yaml b/testdata/generics.v31.yaml index 4e4abc4..1dbf2f9 100644 --- a/testdata/generics.v31.yaml +++ b/testdata/generics.v31.yaml @@ -11,7 +11,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ProfileResponse' + $ref: '#/components/schemas/SpecTestProfileResponse' /user: get: responses: @@ -20,7 +20,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/BaseResponseUser' + $ref: '#/components/schemas/SpecTestBaseResponseUser' /users: post: responses: @@ -29,10 +29,10 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/BaseResponseUserList' + $ref: '#/components/schemas/SpecTestBaseResponseUserList' components: schemas: - BaseResponseUser: + SpecTestBaseResponseUser: type: object properties: data: @@ -48,18 +48,18 @@ components: type: string success: type: boolean - BaseResponseUserList: + SpecTestBaseResponseUserList: type: object properties: data: type: array items: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/SpecTestUser' message: type: string success: type: boolean - ProfileResponse: + SpecTestProfileResponse: type: object properties: data: @@ -75,7 +75,7 @@ components: type: string success: type: boolean - User: + SpecTestUser: type: object required: - id diff --git a/testdata/generics.v32.yaml b/testdata/generics.v32.yaml index 613226d..6a3894c 100644 --- a/testdata/generics.v32.yaml +++ b/testdata/generics.v32.yaml @@ -11,7 +11,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ProfileResponse' + $ref: '#/components/schemas/SpecTestProfileResponse' /user: get: responses: @@ -20,7 +20,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/BaseResponseUser' + $ref: '#/components/schemas/SpecTestBaseResponseUser' /users: post: responses: @@ -29,10 +29,10 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/BaseResponseUserList' + $ref: '#/components/schemas/SpecTestBaseResponseUserList' components: schemas: - BaseResponseUser: + SpecTestBaseResponseUser: type: object properties: data: @@ -48,18 +48,18 @@ components: type: string success: type: boolean - BaseResponseUserList: + SpecTestBaseResponseUserList: type: object properties: data: type: array items: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/SpecTestUser' message: type: string success: type: boolean - ProfileResponse: + SpecTestProfileResponse: type: object properties: data: @@ -75,7 +75,7 @@ components: type: string success: type: boolean - User: + SpecTestUser: type: object required: - id diff --git a/testdata/multipart_binary.v30.yaml b/testdata/multipart_binary.v30.yaml index 668605b..97dd48e 100644 --- a/testdata/multipart_binary.v30.yaml +++ b/testdata/multipart_binary.v30.yaml @@ -21,7 +21,7 @@ paths: content: multipart/form-data: schema: - $ref: '#/components/schemas/UploadRequest' + $ref: '#/components/schemas/SpecTestUploadRequest' encoding: file: contentType: image/png @@ -42,7 +42,7 @@ paths: description: No Content components: schemas: - UploadRequest: + SpecTestUploadRequest: type: object properties: file: diff --git a/testdata/multipart_binary.v31.yaml b/testdata/multipart_binary.v31.yaml index caa85d0..53a03f7 100644 --- a/testdata/multipart_binary.v31.yaml +++ b/testdata/multipart_binary.v31.yaml @@ -21,7 +21,7 @@ paths: content: multipart/form-data: schema: - $ref: '#/components/schemas/UploadRequest' + $ref: '#/components/schemas/SpecTestUploadRequest' encoding: file: contentType: image/png @@ -42,7 +42,7 @@ paths: description: No Content components: schemas: - UploadRequest: + SpecTestUploadRequest: type: object properties: file: diff --git a/testdata/multipart_binary.v32.yaml b/testdata/multipart_binary.v32.yaml index acb40ba..726d2de 100644 --- a/testdata/multipart_binary.v32.yaml +++ b/testdata/multipart_binary.v32.yaml @@ -21,7 +21,7 @@ paths: content: multipart/form-data: schema: - $ref: '#/components/schemas/UploadRequest' + $ref: '#/components/schemas/SpecTestUploadRequest' encoding: file: contentType: image/png @@ -42,7 +42,7 @@ paths: description: No Content components: schemas: - UploadRequest: + SpecTestUploadRequest: type: object properties: file: diff --git a/testdata/multiple_content_types.v30.yaml b/testdata/multiple_content_types.v30.yaml index 08e3ede..5acee19 100644 --- a/testdata/multiple_content_types.v30.yaml +++ b/testdata/multiple_content_types.v30.yaml @@ -10,23 +10,23 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/LoginRequest' + $ref: '#/components/schemas/SpecTestLoginRequest' application/x-www-form-urlencoded: schema: - $ref: '#/components/schemas/LoginRequest' + $ref: '#/components/schemas/SpecTestLoginRequest' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/LoginResponse' + $ref: '#/components/schemas/SpecTestLoginResponse' application/xml: schema: - $ref: '#/components/schemas/LoginResponse' + $ref: '#/components/schemas/SpecTestLoginResponse' components: schemas: - LoginRequest: + SpecTestLoginRequest: type: object required: - username @@ -38,7 +38,7 @@ components: username: type: string minLength: 3 - LoginResponse: + SpecTestLoginResponse: type: object required: - token diff --git a/testdata/multiple_content_types.v31.yaml b/testdata/multiple_content_types.v31.yaml index 2ba6507..3cdb9dc 100644 --- a/testdata/multiple_content_types.v31.yaml +++ b/testdata/multiple_content_types.v31.yaml @@ -10,23 +10,23 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/LoginRequest' + $ref: '#/components/schemas/SpecTestLoginRequest' application/x-www-form-urlencoded: schema: - $ref: '#/components/schemas/LoginRequest' + $ref: '#/components/schemas/SpecTestLoginRequest' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/LoginResponse' + $ref: '#/components/schemas/SpecTestLoginResponse' application/xml: schema: - $ref: '#/components/schemas/LoginResponse' + $ref: '#/components/schemas/SpecTestLoginResponse' components: schemas: - LoginRequest: + SpecTestLoginRequest: type: object required: - username @@ -38,7 +38,7 @@ components: username: type: string minLength: 3 - LoginResponse: + SpecTestLoginResponse: type: object required: - token diff --git a/testdata/multiple_content_types.v32.yaml b/testdata/multiple_content_types.v32.yaml index 3b31740..dbb9d83 100644 --- a/testdata/multiple_content_types.v32.yaml +++ b/testdata/multiple_content_types.v32.yaml @@ -10,23 +10,23 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/LoginRequest' + $ref: '#/components/schemas/SpecTestLoginRequest' application/x-www-form-urlencoded: schema: - $ref: '#/components/schemas/LoginRequest' + $ref: '#/components/schemas/SpecTestLoginRequest' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/LoginResponse' + $ref: '#/components/schemas/SpecTestLoginResponse' application/xml: schema: - $ref: '#/components/schemas/LoginResponse' + $ref: '#/components/schemas/SpecTestLoginResponse' components: schemas: - LoginRequest: + SpecTestLoginRequest: type: object required: - username @@ -38,7 +38,7 @@ components: username: type: string minLength: 3 - LoginResponse: + SpecTestLoginResponse: type: object required: - token diff --git a/testdata/nested_structures.v30.yaml b/testdata/nested_structures.v30.yaml index 180544b..ed243b1 100644 --- a/testdata/nested_structures.v30.yaml +++ b/testdata/nested_structures.v30.yaml @@ -9,13 +9,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/NestedRequest' + $ref: '#/components/schemas/SpecTestNestedRequest' responses: '204': description: No Content components: schemas: - NestedRequest: + SpecTestNestedRequest: type: object properties: level1: diff --git a/testdata/nested_structures.v31.yaml b/testdata/nested_structures.v31.yaml index 4ac2ad3..557015c 100644 --- a/testdata/nested_structures.v31.yaml +++ b/testdata/nested_structures.v31.yaml @@ -9,13 +9,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/NestedRequest' + $ref: '#/components/schemas/SpecTestNestedRequest' responses: '204': description: No Content components: schemas: - NestedRequest: + SpecTestNestedRequest: type: object properties: level1: diff --git a/testdata/nested_structures.v32.yaml b/testdata/nested_structures.v32.yaml index feb1d31..26d9df0 100644 --- a/testdata/nested_structures.v32.yaml +++ b/testdata/nested_structures.v32.yaml @@ -9,13 +9,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/NestedRequest' + $ref: '#/components/schemas/SpecTestNestedRequest' responses: '204': description: No Content components: schemas: - NestedRequest: + SpecTestNestedRequest: type: object properties: level1: diff --git a/testdata/openapi_320_features.v32.yaml b/testdata/openapi_320_features.v32.yaml index f5b0cec..bcfc804 100644 --- a/testdata/openapi_320_features.v32.yaml +++ b/testdata/openapi_320_features.v32.yaml @@ -36,7 +36,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/SpecTestUser' examples: encoded-id: summary: Encoded identifier @@ -68,10 +68,10 @@ paths: schema: type: array items: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/SpecTestUser' components: schemas: - User: + SpecTestUser: type: object required: - id diff --git a/testdata/openapi_320_operations.v32.yaml b/testdata/openapi_320_operations.v32.yaml index 7a88704..748f8d9 100644 --- a/testdata/openapi_320_operations.v32.yaml +++ b/testdata/openapi_320_operations.v32.yaml @@ -19,10 +19,10 @@ paths: schema: type: array items: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/SpecTestUser' components: schemas: - User: + SpecTestUser: type: object required: - id diff --git a/testdata/path_parameters.v30.yaml b/testdata/path_parameters.v30.yaml index 1b4da0d..d977ca6 100644 --- a/testdata/path_parameters.v30.yaml +++ b/testdata/path_parameters.v30.yaml @@ -22,10 +22,10 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/SpecTestUser' components: schemas: - User: + SpecTestUser: type: object required: - id diff --git a/testdata/path_parameters.v31.yaml b/testdata/path_parameters.v31.yaml index f456851..7dd180c 100644 --- a/testdata/path_parameters.v31.yaml +++ b/testdata/path_parameters.v31.yaml @@ -22,10 +22,10 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/SpecTestUser' components: schemas: - User: + SpecTestUser: type: object required: - id diff --git a/testdata/path_parameters.v32.yaml b/testdata/path_parameters.v32.yaml index 7ba95ca..851c66f 100644 --- a/testdata/path_parameters.v32.yaml +++ b/testdata/path_parameters.v32.yaml @@ -22,10 +22,10 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/SpecTestUser' components: schemas: - User: + SpecTestUser: type: object required: - id diff --git a/testdata/request_response.v30.yaml b/testdata/request_response.v30.yaml index ab8104c..58eed5a 100644 --- a/testdata/request_response.v30.yaml +++ b/testdata/request_response.v30.yaml @@ -12,17 +12,17 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/LoginRequest' + $ref: '#/components/schemas/SpecTestLoginRequest' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/LoginResponse' + $ref: '#/components/schemas/SpecTestLoginResponse' components: schemas: - LoginRequest: + SpecTestLoginRequest: type: object required: - username @@ -34,7 +34,7 @@ components: username: type: string minLength: 3 - LoginResponse: + SpecTestLoginResponse: type: object required: - token diff --git a/testdata/request_response.v31.yaml b/testdata/request_response.v31.yaml index c4ccf4d..d2e2222 100644 --- a/testdata/request_response.v31.yaml +++ b/testdata/request_response.v31.yaml @@ -12,17 +12,17 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/LoginRequest' + $ref: '#/components/schemas/SpecTestLoginRequest' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/LoginResponse' + $ref: '#/components/schemas/SpecTestLoginResponse' components: schemas: - LoginRequest: + SpecTestLoginRequest: type: object required: - username @@ -34,7 +34,7 @@ components: username: type: string minLength: 3 - LoginResponse: + SpecTestLoginResponse: type: object required: - token diff --git a/testdata/request_response.v32.yaml b/testdata/request_response.v32.yaml index d6f97d4..ca07f88 100644 --- a/testdata/request_response.v32.yaml +++ b/testdata/request_response.v32.yaml @@ -12,17 +12,17 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/LoginRequest' + $ref: '#/components/schemas/SpecTestLoginRequest' responses: '200': description: OK content: application/json: schema: - $ref: '#/components/schemas/LoginResponse' + $ref: '#/components/schemas/SpecTestLoginResponse' components: schemas: - LoginRequest: + SpecTestLoginRequest: type: object required: - username @@ -34,7 +34,7 @@ components: username: type: string minLength: 3 - LoginResponse: + SpecTestLoginResponse: type: object required: - token diff --git a/testdata/security.v30.yaml b/testdata/security.v30.yaml index 276406b..66ad36c 100644 --- a/testdata/security.v30.yaml +++ b/testdata/security.v30.yaml @@ -11,12 +11,12 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/SpecTestUser' security: - bearerAuth: [] components: schemas: - User: + SpecTestUser: type: object required: - id diff --git a/testdata/security.v31.yaml b/testdata/security.v31.yaml index 36a150e..46c8fe4 100644 --- a/testdata/security.v31.yaml +++ b/testdata/security.v31.yaml @@ -11,12 +11,12 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/SpecTestUser' security: - bearerAuth: [] components: schemas: - User: + SpecTestUser: type: object required: - id diff --git a/testdata/security.v32.yaml b/testdata/security.v32.yaml index 5cda3ad..70e1825 100644 --- a/testdata/security.v32.yaml +++ b/testdata/security.v32.yaml @@ -11,12 +11,12 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/SpecTestUser' security: - bearerAuth: [] components: schemas: - User: + SpecTestUser: type: object required: - id From e464f9c96629555080f075a3fe3f6e10b0ae37c3 Mon Sep 17 00:00:00 2001 From: Ahmad Faiz Kamaludin Date: Tue, 12 May 2026 08:34:40 +0700 Subject: [PATCH 7/8] ci: update linter to use action and adjust security to upload sarif --- .github/workflows/ci.yml | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 314d046..dd56e05 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,22 +34,14 @@ jobs: check-latest: true # Always grab the newest patch release cache: true # Handles ~/.cache/go-build and ~/go/pkg/mod - - name: 📁 Cache Tools - uses: actions/cache@v4 - with: - path: | - ~/.cache/golangci-lint - ~/go/bin - key: ${{ runner.os }}-tools-${{ env.CACHE_VERSION }}-${{ hashFiles('tools/tools.go', 'Makefile') }} - restore-keys: | - ${{ runner.os }}-tools-${{ env.CACHE_VERSION }}- - ${{ runner.os }}-tools- - - - name: 🔧 Install Tools - run: make install-tools + - name: ✅ Sync & Tidy + run: make sync tidy - - name: ✅ Run Quality Checks - run: make sync tidy lint + - name: 🔍 Lint + uses: golangci/golangci-lint-action@v9 + with: + version: v2.12.2 + experimental: "automatic-module-directories" # ======================================== # Test Matrix @@ -119,7 +111,12 @@ jobs: - name: Run Gosec Security Scanner uses: securego/gosec@v2.26.1 with: - args: './...' + args: '-no-fail -fmt sarif -out results.sarif ./...' + + - name: Upload SARIF to GitHub + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: results.sarif # ======================================== # Final Status Check From 7475bb6baff88c9416932ba276f1d519b6ad54ee Mon Sep 17 00:00:00 2001 From: Ahmad Faiz Kamaludin Date: Tue, 12 May 2026 08:46:26 +0700 Subject: [PATCH 8/8] ci: update lint mechanism to use matrix strategy --- .github/workflows/ci.yml | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd56e05..58e2dc2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,10 +19,25 @@ jobs: # Quality Gate # ======================================== quality: - name: 🛡️ Quality Gate + name: 🛡️ Quality Gate / ${{ matrix.module }} runs-on: ubuntu-latest timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + module: + - . + - adapter/chiopenapi + - adapter/echoopenapi + - adapter/echov5openapi + - adapter/fiberopenapi + - adapter/fiberv3openapi + - adapter/ginopenapi + - adapter/httpopenapi + - adapter/irisopenapi + - adapter/muxopenapi + steps: - name: 📥 Checkout uses: actions/checkout@v4 @@ -30,9 +45,9 @@ jobs: - name: 🐹 Setup Go uses: actions/setup-go@v5 with: - go-version: '1.25.x' # Use latest supported toolchain for quality checks - check-latest: true # Always grab the newest patch release - cache: true # Handles ~/.cache/go-build and ~/go/pkg/mod + go-version: '1.25.x' + check-latest: true + cache: true - name: ✅ Sync & Tidy run: make sync tidy @@ -41,7 +56,7 @@ jobs: uses: golangci/golangci-lint-action@v9 with: version: v2.12.2 - experimental: "automatic-module-directories" + working-directory: ${{ matrix.module }} # ======================================== # Test Matrix