diff --git a/application_test.go b/application_test.go index 977c5d2..b895b08 100644 --- a/application_test.go +++ b/application_test.go @@ -51,7 +51,7 @@ func TestMain(m *testing.M) { func TestApplication_LoadCode(t *testing.T) { sctx := loadClassificationPkgsCtx(t) require.NotNil(t, sctx) - require.Len(t, sctx.app.Models, 39) + require.Len(t, sctx.app.Models, 45) require.Len(t, sctx.app.Meta, 1) require.Len(t, sctx.app.Routes, 7) require.Empty(t, sctx.app.Operations) diff --git a/fixtures/goparsing/classification/models/nomodel.go b/fixtures/goparsing/classification/models/nomodel.go index 2750761..d2c169c 100644 --- a/fixtures/goparsing/classification/models/nomodel.go +++ b/fixtures/goparsing/classification/models/nomodel.go @@ -799,3 +799,52 @@ type NamedMapOfStoreOrderSlices GenericMap[string, GenericSlice[StoreOrder]] // // End of models related to named types with type arguments // + +// SomeStringSlice is a named slice type with swagger:type array. +// swagger:type array +type SomeStringSlice []string + +// swagger:model namedWithArrayType +type NamedWithArrayType struct { + // Tags for this item. + Tags SomeStringSlice `json:"tags"` +} + +// SomeFixedArray is a named fixed-length array type with swagger:type array. +// swagger:type array +type SomeFixedArray [5]string + +// swagger:model namedWithFixedArrayType +type NamedWithFixedArrayType struct { + // Labels for this item. + Labels SomeFixedArray `json:"labels"` +} + +// SomeStructWithBadType is a struct type with an unsupported swagger:type value. +// The unsupported value should be ignored and the type should fall through to $ref. +// +// swagger:type badvalue +// swagger:model someStructWithBadType +type SomeStructWithBadType struct { + Name string `json:"name"` +} + +// swagger:model namedWithBadStructType +type NamedWithBadStructType struct { + // The nested struct with an unsupported swagger:type. + Nested SomeStructWithBadType `json:"nested"` +} + +// SomeObjectType is a struct with swagger:type object. +// swagger:type object +// swagger:model someObjectType +type SomeObjectType struct { + Key string `json:"key"` + Value string `json:"value"` +} + +// swagger:model namedWithObjectStructType +type NamedWithObjectStructType struct { + // Headers for this request. + Headers SomeObjectType `json:"headers"` +} diff --git a/schema.go b/schema.go index 83bbfb9..099c1c7 100644 --- a/schema.go +++ b/schema.go @@ -429,10 +429,15 @@ func (s *schemaBuilder) buildNamedType(titpe *types.Named, tgt swaggerTypable) e cmt = new(ast.CommentGroup) } - if typeName, ok := typeName(cmt); ok { - _ = swaggerSchemaForType(typeName, tgt) - - return nil + if tn, ok := typeName(cmt); ok { + if err := swaggerSchemaForType(tn, tgt); err == nil { + return nil + } + // For unsupported swagger:type values (e.g., "array"), fall through + // to underlying type resolution so the full schema (including items + // for slices) is properly built. Build directly from the underlying + // type to bypass the named-type $ref creation. + return s.buildFromType(titpe.Underlying(), tgt) } if s.decl.Spec.Assign.IsValid() { @@ -559,9 +564,12 @@ func (s *schemaBuilder) buildNamedStruct(tio *types.TypeName, cmt *ast.CommentGr return nil } - if typeName, ok := typeName(cmt); ok { - _ = swaggerSchemaForType(typeName, tgt) - return nil + if tn, ok := typeName(cmt); ok { + if err := swaggerSchemaForType(tn, tgt); err == nil { + return nil + } + // For unsupported swagger:type values, fall through to makeRef + // rather than silently returning an empty schema. } return s.makeRef(decl, tgt) @@ -583,6 +591,14 @@ func (s *schemaBuilder) buildNamedArray(tio *types.TypeName, cmt *ast.CommentGro tgt.Items().Typed("string", sfnm) return nil } + // When swagger:type is set to an unsupported value (e.g., "array"), + // skip the $ref and inline the array schema with proper items type. + if tn, ok := typeName(cmt); ok { + if err := swaggerSchemaForType(tn, tgt); err != nil { + return s.buildFromType(elem, tgt.Items()) + } + return nil + } if decl, ok := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()); ok { return s.makeRef(decl, tgt) } @@ -600,6 +616,16 @@ func (s *schemaBuilder) buildNamedSlice(tio *types.TypeName, cmt *ast.CommentGro tgt.Items().Typed("string", sfnm) return nil } + // When swagger:type is set to an unsupported value (e.g., "array"), + // skip the $ref and inline the slice schema with proper items type. + // This preserves the field's description that would be lost with $ref. + if tn, ok := typeName(cmt); ok { + if err := swaggerSchemaForType(tn, tgt); err != nil { + // Unsupported type name (e.g., "array") — build inline from element type. + return s.buildFromType(elem, tgt.Items()) + } + return nil + } if decl, ok := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()); ok { return s.makeRef(decl, tgt) } diff --git a/schema_test.go b/schema_test.go index 4d52589..96a44cd 100644 --- a/schema_test.go +++ b/schema_test.go @@ -2582,3 +2582,80 @@ func TestSetEnumDoesNotPanic(t *testing.T) { require.NoError(t, err) } + +func TestSwaggerTypeNamedArray(t *testing.T) { + sctx := loadClassificationPkgsCtx(t) + decl := getClassificationModel(sctx, "NamedWithArrayType") + require.NotNil(t, decl) + prs := &schemaBuilder{ + ctx: sctx, + decl: decl, + } + models := make(map[string]spec.Schema) + require.NoError(t, prs.Build(models)) + schema := models["namedWithArrayType"] + + // swagger:type array on a named []string type should produce + // an inlined array with string items, not a $ref. + assertArrayProperty(t, &schema, "string", "tags", "", "Tags") +} + +func TestSwaggerTypeNamedFixedArray(t *testing.T) { + sctx := loadClassificationPkgsCtx(t) + decl := getClassificationModel(sctx, "NamedWithFixedArrayType") + require.NotNil(t, decl) + prs := &schemaBuilder{ + ctx: sctx, + decl: decl, + } + models := make(map[string]spec.Schema) + require.NoError(t, prs.Build(models)) + schema := models["namedWithFixedArrayType"] + + // swagger:type array on a named [5]string type should produce + // an inlined array with string items via buildNamedArray. + assertArrayProperty(t, &schema, "string", "labels", "", "Labels") +} + +func TestSwaggerTypeBadValueOnStruct(t *testing.T) { + sctx := loadClassificationPkgsCtx(t) + decl := getClassificationModel(sctx, "NamedWithBadStructType") + require.NotNil(t, decl) + prs := &schemaBuilder{ + ctx: sctx, + decl: decl, + } + models := make(map[string]spec.Schema) + require.NoError(t, prs.Build(models)) + schema := models["namedWithBadStructType"] + + // swagger:type with an unsupported value on a struct should not + // produce an empty schema — it should either inline the struct + // or create a $ref. The key assertion is that the property exists + // and is not empty (i.e., the error was not silently swallowed). + prop := schema.Properties["nested"] + hasType := len(prop.Type) > 0 + hasRef := prop.Ref.String() != "" + hasProps := len(prop.Properties) > 0 + assert.TrueT(t, hasType || hasRef || hasProps, + "expected nested property to have type, $ref, or properties — not an empty schema") +} + +func TestSwaggerTypeObjectOnStruct(t *testing.T) { + sctx := loadClassificationPkgsCtx(t) + decl := getClassificationModel(sctx, "NamedWithObjectStructType") + require.NotNil(t, decl) + prs := &schemaBuilder{ + ctx: sctx, + decl: decl, + } + models := make(map[string]spec.Schema) + require.NoError(t, prs.Build(models)) + schema := models["namedWithObjectStructType"] + + // swagger:type object on a struct should inline as type:object, + // preserving the field's description. + prop := schema.Properties["headers"] + assert.TrueT(t, prop.Type.Contains("object")) + assert.Empty(t, prop.Ref.String(), "should not have $ref when swagger:type object is set") +}