diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 314d046..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,26 +45,18 @@ 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 - - - 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- + go-version: '1.25.x' + check-latest: true + cache: true - - 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 + working-directory: ${{ matrix.module }} # ======================================== # Test Matrix @@ -119,7 +126,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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d7e7c8..cc8d398 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,32 @@ 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. +- `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`). + +### 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. +- `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 +104,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 diff --git a/README.md b/README.md index 8aa7eeb..ca10029 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 @@ -491,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. | @@ -585,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/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/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..f84ec21 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 @@ -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 @@ -222,7 +238,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Fiberv3openapi_testAPIResponse' + $ref: '#/components/schemas/DtoAPIResponse' security: - petstore_auth: - write:pets @@ -238,14 +254,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 +282,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Fiberv3openapi_testOrder' + $ref: '#/components/schemas/DtoOrder' '404': description: Not Found delete: @@ -296,14 +312,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 +333,7 @@ paths: schema: type: array items: - $ref: '#/components/schemas/Fiberv3openapi_testPetUser' + $ref: '#/components/schemas/DtoPetUser' responses: '201': description: Created @@ -340,7 +356,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Fiberv3openapi_testPetUser' + $ref: '#/components/schemas/DtoPetUser' '404': description: Not Found put: @@ -389,7 +405,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Fiberv3openapi_testPetUser' + $ref: '#/components/schemas/DtoPetUser' '404': description: Not Found delete: @@ -409,7 +425,7 @@ paths: description: No Content components: schemas: - Fiberv3openapi_testAPIResponse: + DtoAPIResponse: type: object properties: code: @@ -419,7 +435,7 @@ components: type: string type: type: string - Fiberv3openapi_testOrder: + DtoOrder: type: object properties: complete: @@ -442,7 +458,7 @@ components: - placed - approved - delivered - Fiberv3openapi_testPet: + DtoPet: type: object properties: category: @@ -471,10 +487,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 +515,7 @@ components: - 2 username: type: string - Fiberv3openapi_testTag: + DtoTag: type: object properties: id: 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/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/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/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/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/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/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..557f0aa 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 { @@ -91,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"]) } @@ -104,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"]) }) @@ -116,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) }) @@ -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/ReflectTestEmbedBase", 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/ReflectTestEmbedBaseViaInterface", 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}) @@ -266,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.go b/internal/reflect/reflector.go index 2a8eb91..3574fa8 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) { @@ -61,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) { @@ -109,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 } } @@ -182,6 +196,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 +205,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 +229,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 +239,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 } @@ -236,8 +255,10 @@ 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) if _, err := interceptSchema(postParams); err != nil { delete(r.Generating, t) delete(r.Components, name) @@ -256,6 +277,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 @@ -277,6 +314,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 +487,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/reflect/reflector_test.go b/internal/reflect/reflector_test.go index 3cd51e1..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") }) @@ -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) @@ -243,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) }) } @@ -486,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) { @@ -508,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) { @@ -642,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) }) } @@ -666,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 273a3c2..9d79393 100644 --- a/internal/reflect/utils.go +++ b/internal/reflect/utils.go @@ -2,6 +2,7 @@ package reflect import ( "fmt" + "mime/multipart" "path" "reflect" "regexp" @@ -16,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) } @@ -38,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 { @@ -68,6 +79,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 @@ -89,6 +121,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) @@ -96,7 +141,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) @@ -150,3 +197,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..36655c7 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" @@ -70,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()) }) @@ -113,3 +111,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{})) +} 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..7916d1f 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" @@ -11,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 { @@ -63,6 +72,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 @@ -107,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/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" ) 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/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 445c714..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 } @@ -310,9 +265,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 +317,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 { 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 new file mode 100644 index 0000000..2b5686f --- /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/SpecTestEmbedRefMixedRequest' + responses: + '204': + description: No Content + /refer-tag: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SpecTestEmbedRefRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/SpecTestEmbedRefBase' +components: + schemas: + SpecTestEmbedRefBase: + type: object + properties: + id: + type: integer + format: int32 + role: + type: string + SpecTestEmbedRefBaseViaInterface: + type: object + properties: + tag: + type: string + SpecTestEmbedRefMixedRequest: + type: object + properties: + code: + type: string + value: + type: integer + format: int32 + allOf: + - $ref: '#/components/schemas/SpecTestEmbedRefBaseViaInterface' + SpecTestEmbedRefRequest: + type: object + properties: + age: + type: integer + format: int32 + name: + type: string + allOf: + - $ref: '#/components/schemas/SpecTestEmbedRefBase' diff --git a/testdata/embed_ref.v31.yaml b/testdata/embed_ref.v31.yaml new file mode 100644 index 0000000..a6b715d --- /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/SpecTestEmbedRefMixedRequest' + responses: + '204': + description: No Content + /refer-tag: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SpecTestEmbedRefRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/SpecTestEmbedRefBase' +components: + schemas: + SpecTestEmbedRefBase: + type: object + properties: + id: + type: integer + format: int32 + role: + type: string + SpecTestEmbedRefBaseViaInterface: + type: object + properties: + tag: + type: string + SpecTestEmbedRefMixedRequest: + type: object + properties: + code: + type: string + value: + type: integer + format: int32 + allOf: + - $ref: '#/components/schemas/SpecTestEmbedRefBaseViaInterface' + SpecTestEmbedRefRequest: + type: object + properties: + age: + type: integer + format: int32 + name: + type: string + allOf: + - $ref: '#/components/schemas/SpecTestEmbedRefBase' diff --git a/testdata/embed_ref.v32.yaml b/testdata/embed_ref.v32.yaml new file mode 100644 index 0000000..b56518f --- /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/SpecTestEmbedRefMixedRequest' + responses: + '204': + description: No Content + /refer-tag: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SpecTestEmbedRefRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/SpecTestEmbedRefBase' +components: + schemas: + SpecTestEmbedRefBase: + type: object + properties: + id: + type: integer + format: int32 + role: + type: string + SpecTestEmbedRefBaseViaInterface: + type: object + properties: + tag: + type: string + SpecTestEmbedRefMixedRequest: + type: object + properties: + code: + type: string + value: + type: integer + format: int32 + allOf: + - $ref: '#/components/schemas/SpecTestEmbedRefBaseViaInterface' + SpecTestEmbedRefRequest: + type: object + properties: + age: + type: integer + format: int32 + name: + type: string + allOf: + - $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