diff --git a/jsonschema.go b/jsonschema.go index 8d0fb02..033c989 100644 --- a/jsonschema.go +++ b/jsonschema.go @@ -2,15 +2,25 @@ package spec import ( "fmt" + "reflect" + "regexp" + "strings" "github.com/oaswrap/spec/internal/debuglog" "github.com/oaswrap/spec/openapi" "github.com/swaggest/jsonschema-go" ) +var genericInstRe = regexp.MustCompile(`^(\w+)\[(.+)\]$`) + func getJSONSchemaOpts(cfg *openapi.ReflectorConfig, logger *debuglog.Logger) []func(*jsonschema.ReflectContext) { var opts []func(*jsonschema.ReflectContext) + if cfg == nil { + opts = append(opts, jsonschema.InterceptDefName(shortenGenericName)) + return opts + } + if cfg.InlineRefs { opts = append(opts, jsonschema.InlineRefs) logger.Printf("set inline references to true") @@ -27,6 +37,7 @@ func getJSONSchemaOpts(cfg *openapi.ReflectorConfig, logger *debuglog.Logger) [] opts = append(opts, jsonschema.StripDefinitionNamePrefix(cfg.StripDefNamePrefix...)) logger.LogAction("set strip definition name prefix", fmt.Sprintf("%v", cfg.StripDefNamePrefix)) } + opts = append(opts, jsonschema.InterceptDefName(shortenGenericName)) if cfg.InterceptDefNameFunc != nil { opts = append(opts, jsonschema.InterceptDefName(cfg.InterceptDefNameFunc)) logger.Printf("set custom intercept definition name function") @@ -63,3 +74,36 @@ func getJSONSchemaOpts(cfg *openapi.ReflectorConfig, logger *debuglog.Logger) [] return opts } + +// shortenGenericName converts "Page[some/pkg.Item]" to "PageItem". +func shortenGenericName(t reflect.Type, defaultDefName string) string { + m := genericInstRe.FindStringSubmatch(t.Name()) + if m == nil { + return defaultDefName + } + // Use the container name from defaultDefName, which already has the package + // prefix applied and StripDefinitionNamePrefix already run — so the result + // is consistent with how non-generic struct names are generated. + containerName := m[1] + if before, _, found := strings.Cut(defaultDefName, "["); found { + containerName = before + } + args := strings.Split(m[2], ", ") + result := containerName + var sb strings.Builder + for _, arg := range args { + arg = strings.TrimPrefix(arg, "*") + var suffixSb strings.Builder + for strings.HasPrefix(arg, "[]") { + suffixSb.WriteString("List") + arg = arg[2:] + } + arg = strings.TrimPrefix(arg, "*") + if i := strings.LastIndex(arg, "."); i >= 0 { + arg = arg[i+1:] + } + sb.WriteString(arg + suffixSb.String()) + } + result += sb.String() + return result +} diff --git a/reflector3.go b/reflector3.go index 4960cf7..ed51dbe 100644 --- a/reflector3.go +++ b/reflector3.go @@ -89,13 +89,12 @@ func newReflector3(cfg *openapi.Config, logger *debuglog.Logger) reflector { var parameterTagMapping map[openapi.ParameterIn]string - // Custom options for JSON schema generation - if cfg.ReflectorConfig != nil { - jsonSchemaOpts := getJSONSchemaOpts(cfg.ReflectorConfig, logger) - if len(jsonSchemaOpts) > 0 { - reflector.DefaultOptions = append(reflector.DefaultOptions, jsonSchemaOpts...) - } + jsonSchemaOpts := getJSONSchemaOpts(cfg.ReflectorConfig, logger) + if len(jsonSchemaOpts) > 0 { + reflector.DefaultOptions = append(reflector.DefaultOptions, jsonSchemaOpts...) + } + if cfg.ReflectorConfig != nil { for _, opt := range cfg.ReflectorConfig.TypeMappings { reflector.AddTypeMapping(opt.Src, opt.Dst) logger.LogAction("add type mapping", fmt.Sprintf("%T -> %T", opt.Src, opt.Dst)) diff --git a/reflector31.go b/reflector31.go index a5644cc..b76d9c5 100644 --- a/reflector31.go +++ b/reflector31.go @@ -86,13 +86,12 @@ func newReflector31(cfg *openapi.Config, logger *debuglog.Logger) reflector { var parameterTagMapping map[openapi.ParameterIn]string - // Custom options for JSON schema generation - if cfg.ReflectorConfig != nil { - jsonSchemaOpts := getJSONSchemaOpts(cfg.ReflectorConfig, logger) - if len(jsonSchemaOpts) > 0 { - reflector.DefaultOptions = append(reflector.DefaultOptions, jsonSchemaOpts...) - } + jsonSchemaOpts := getJSONSchemaOpts(cfg.ReflectorConfig, logger) + if len(jsonSchemaOpts) > 0 { + reflector.DefaultOptions = append(reflector.DefaultOptions, jsonSchemaOpts...) + } + if cfg.ReflectorConfig != nil { for _, opt := range cfg.ReflectorConfig.TypeMappings { reflector.AddTypeMapping(opt.Src, opt.Dst) logger.LogAction("add type mapping", fmt.Sprintf("%T -> %T", opt.Src, opt.Dst)) diff --git a/router_test.go b/router_test.go index 418d1d7..62c6dc6 100644 --- a/router_test.go +++ b/router_test.go @@ -203,6 +203,11 @@ func TestRouter(t *testing.T) { Name: "Authentication", Description: "Operations related to user authentication", }), + option.WithReflectorConfig( + option.TypeMapping(NullString{}, new(string)), + option.TypeMapping(NullTime{}, new(time.Time)), + ), + option.WithSecurity("bearerAuth", option.SecurityHTTPBearer("Bearer")), }, setup: func(r spec.Router) { r.Post("/login", @@ -213,6 +218,38 @@ func TestRouter(t *testing.T) { option.Request(new(LoginRequest)), option.Response(200, new(Response[Token])), ) + r.Get("/user", + option.OperationID("getUserProfile"), + option.Summary("Get User Profile"), + option.Description("This operation retrieves the authenticated user's profile."), + option.Tags("Authentication"), + option.Security("bearerAuth"), + option.Response(200, new(Response[User])), + ) + r.Get("/users", + option.OperationID("getUsers"), + option.Summary("Get Users"), + option.Description("This operation retrieves a list of users."), + option.Tags("Authentication"), + option.Security("bearerAuth"), + option.Response(200, new(Response[[]User])), + ) + r.Get("/nested-generic-user", + option.OperationID("getNestedGenericUser"), + option.Summary("Get Nested Generic User"), + option.Description("This operation retrieves a nested generic user."), + option.Tags("Authentication"), + option.Security("bearerAuth"), + option.Response(200, new(Response[Response[User]])), + ) + r.Get("/nested-generic-users", + option.OperationID("getNestedGenericUsers"), + option.Summary("Get Nested Generic Users"), + option.Description("This operation retrieves a nested generic users."), + option.Tags("Authentication"), + option.Security("bearerAuth"), + option.Response(200, new(Response[Response[[]User]])), + ) }, }, { diff --git a/testdata/all_operation_options_3.yaml b/testdata/all_operation_options_3.yaml index 8def7e5..9d7703d 100644 --- a/testdata/all_operation_options_3.yaml +++ b/testdata/all_operation_options_3.yaml @@ -20,7 +20,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/SpecTestResponseGithubComOaswrapSpecTestUser' + $ref: '#/components/schemas/SpecTestResponseUser' description: Response body for operation options security: - apiKey: [] @@ -45,7 +45,7 @@ components: type: object SpecTestNullTime: type: object - SpecTestResponseGithubComOaswrapSpecTestUser: + SpecTestResponseUser: properties: data: $ref: '#/components/schemas/SpecTestUser' diff --git a/testdata/all_operation_options_31.yaml b/testdata/all_operation_options_31.yaml index a874ed0..56c405c 100644 --- a/testdata/all_operation_options_31.yaml +++ b/testdata/all_operation_options_31.yaml @@ -20,7 +20,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/SpecTestResponseGithubComOaswrapSpecTestUser' + $ref: '#/components/schemas/SpecTestResponseUser' description: Response body for operation options security: - apiKey: [] @@ -47,7 +47,7 @@ components: type: object SpecTestNullTime: type: object - SpecTestResponseGithubComOaswrapSpecTestUser: + SpecTestResponseUser: properties: data: $ref: '#/components/schemas/SpecTestUser' diff --git a/testdata/generic_response_3.yaml b/testdata/generic_response_3.yaml index 162330e..d83757d 100644 --- a/testdata/generic_response_3.yaml +++ b/testdata/generic_response_3.yaml @@ -21,11 +21,75 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/SpecTestResponseGithubComOaswrapSpecTestToken' + $ref: '#/components/schemas/SpecTestResponseToken' description: OK summary: User Login tags: - Authentication + /nested-generic-user: + get: + description: This operation retrieves a nested generic user. + operationId: getNestedGenericUser + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/SpecTestResponseUserType2' + description: OK + security: + - bearerAuth: [] + summary: Get Nested Generic User + tags: + - Authentication + /nested-generic-users: + get: + description: This operation retrieves a nested generic users. + operationId: getNestedGenericUsers + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/SpecTestResponseUserType3' + description: OK + security: + - bearerAuth: [] + summary: Get Nested Generic Users + tags: + - Authentication + /user: + get: + description: This operation retrieves the authenticated user's profile. + operationId: getUserProfile + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/SpecTestResponseUser' + description: OK + security: + - bearerAuth: [] + summary: Get User Profile + tags: + - Authentication + /users: + get: + description: This operation retrieves a list of users. + operationId: getUsers + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/SpecTestResponseUserList' + description: OK + security: + - bearerAuth: [] + summary: Get Users + tags: + - Authentication components: schemas: SpecTestLoginRequest: @@ -40,7 +104,7 @@ components: - username - password type: object - SpecTestResponseGithubComOaswrapSpecTestToken: + SpecTestResponseToken: properties: data: $ref: '#/components/schemas/SpecTestToken' @@ -48,9 +112,66 @@ components: example: 200 type: integer type: object + SpecTestResponseUser: + properties: + data: + $ref: '#/components/schemas/SpecTestUser' + status: + example: 200 + type: integer + type: object + SpecTestResponseUserList: + properties: + data: + items: + $ref: '#/components/schemas/SpecTestUser' + nullable: true + type: array + status: + example: 200 + type: integer + type: object + SpecTestResponseUserType2: + properties: + data: + $ref: '#/components/schemas/SpecTestResponseUser' + status: + example: 200 + type: integer + type: object + SpecTestResponseUserType3: + properties: + data: + $ref: '#/components/schemas/SpecTestResponseUserList' + status: + example: 200 + type: integer + type: object SpecTestToken: properties: token: example: abc123 type: string type: object + SpecTestUser: + properties: + age: + nullable: true + type: integer + created_at: + format: date-time + type: string + email: + type: string + id: + type: integer + updated_at: + format: date-time + type: string + username: + type: string + type: object + securitySchemes: + bearerAuth: + scheme: Bearer + type: http diff --git a/testdata/generic_response_31.yaml b/testdata/generic_response_31.yaml index bbf0ce0..55d5603 100644 --- a/testdata/generic_response_31.yaml +++ b/testdata/generic_response_31.yaml @@ -18,11 +18,75 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/SpecTestResponseGithubComOaswrapSpecTestToken' + $ref: '#/components/schemas/SpecTestResponseToken' description: OK summary: User Login tags: - Authentication + /nested-generic-user: + get: + description: This operation retrieves a nested generic user. + operationId: getNestedGenericUser + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/SpecTestResponseUserType2' + description: OK + security: + - bearerAuth: [] + summary: Get Nested Generic User + tags: + - Authentication + /nested-generic-users: + get: + description: This operation retrieves a nested generic users. + operationId: getNestedGenericUsers + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/SpecTestResponseUserType3' + description: OK + security: + - bearerAuth: [] + summary: Get Nested Generic Users + tags: + - Authentication + /user: + get: + description: This operation retrieves the authenticated user's profile. + operationId: getUserProfile + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/SpecTestResponseUser' + description: OK + security: + - bearerAuth: [] + summary: Get User Profile + tags: + - Authentication + /users: + get: + description: This operation retrieves a list of users. + operationId: getUsers + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/SpecTestResponseUserList' + description: OK + security: + - bearerAuth: [] + summary: Get Users + tags: + - Authentication components: schemas: SpecTestLoginRequest: @@ -39,7 +103,7 @@ components: - username - password type: object - SpecTestResponseGithubComOaswrapSpecTestToken: + SpecTestResponseToken: properties: data: $ref: '#/components/schemas/SpecTestToken' @@ -48,6 +112,46 @@ components: - 200 type: integer type: object + SpecTestResponseUser: + properties: + data: + $ref: '#/components/schemas/SpecTestUser' + status: + examples: + - 200 + type: integer + type: object + SpecTestResponseUserList: + properties: + data: + items: + $ref: '#/components/schemas/SpecTestUser' + type: + - array + - "null" + status: + examples: + - 200 + type: integer + type: object + SpecTestResponseUserType2: + properties: + data: + $ref: '#/components/schemas/SpecTestResponseUser' + status: + examples: + - 200 + type: integer + type: object + SpecTestResponseUserType3: + properties: + data: + $ref: '#/components/schemas/SpecTestResponseUserList' + status: + examples: + - 200 + type: integer + type: object SpecTestToken: properties: token: @@ -55,6 +159,29 @@ components: - abc123 type: string type: object + SpecTestUser: + properties: + age: + type: + - "null" + - integer + created_at: + format: date-time + type: string + email: + type: string + id: + type: integer + updated_at: + format: date-time + type: string + username: + type: string + type: object + securitySchemes: + bearerAuth: + scheme: Bearer + type: http tags: - description: Operations related to user authentication name: Authentication