Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
coverage:
status:
patch:
default:
target: 80%
ignore:
- internal/scantest
- fixtures
1 change: 0 additions & 1 deletion fixtures/goparsing/spec/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,4 @@ type BookingResponse struct {
// Responses:
// 200: BookingResponse
func bookings(w http.ResponseWriter, r *http.Request) {

}
2 changes: 1 addition & 1 deletion internal/builders/items/typable.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func NewTypable(items *oaispec.Items, level int, in string) Typable {
}
}

func (pt Typable) In() string { return pt.in } // TODO(fred): inherit from param
func (pt Typable) In() string { return pt.in }

func (pt Typable) Level() int { return pt.level }

Expand Down
2 changes: 1 addition & 1 deletion internal/builders/parameters/parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func (p *ParameterBuilder) buildFromField(fld *types.Var, tpe types.Type, typabl
case *types.Named:
return p.buildNamedField(ftpe, typable)
case *types.Alias:
logger.DebugLogf(p.ctx.Debug(), "alias(parameters.buildFromField): got alias %v to %v", ftpe, ftpe.Rhs()) // TODO
logger.DebugLogf(p.ctx.Debug(), "alias(parameters.buildFromField): got alias %v to %v", ftpe, ftpe.Rhs())
return p.buildFieldAlias(ftpe, typable, fld, seen)
default:
return fmt.Errorf("unknown type for %s: %T: %w", fld.String(), fld.Type(), ErrParameters)
Expand Down
19 changes: 6 additions & 13 deletions internal/builders/parameters/taggers.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,15 @@ import (
)

func setupParamTaggers(param *oaispec.Parameter, name string, afld *ast.Field, skipExt, debug bool) ([]parsers.TagParser, error) {
if param.Ref.String() != "" {
return setupRefParamTaggers(param, skipExt, debug), nil
}

// Parameter-level $ref (e.g. {$ref: "#/parameters/X"}) is not emitted by
// the scanner today — named struct fields become body params with a
// schema-level ref (ps.Schema.Ref), never ps.Ref. To support
// operation-level parameter refs, branch here on
// `param.Ref.String() != ""` and dispatch to a narrower tagger set
// (in, required, extensions only).
return setupInlineParamTaggers(param, name, afld, skipExt, debug)
}

// setupRefParamTaggers configures taggers for a parameter that is a $ref.
func setupRefParamTaggers(param *oaispec.Parameter, skipExt, debug bool) []parsers.TagParser {
return []parsers.TagParser{
parsers.NewSingleLineTagParser("in", parsers.NewMatchParamIn(param)),
parsers.NewSingleLineTagParser("required", parsers.NewMatchParamRequired(param)),
parsers.NewMultiLineTagParser("Extensions", parsers.NewSetExtensions(spExtensionsSetter(param, skipExt), debug), true),
}
}

// baseInlineParamTaggers configures taggers for a fully-defined inline parameter.
func baseInlineParamTaggers(param *oaispec.Parameter, skipExt, debug bool) []parsers.TagParser {
return []parsers.TagParser{
Expand Down
5 changes: 3 additions & 2 deletions internal/builders/resolvers/resolvers.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const (
)

// SwaggerSchemaForType maps all Go builtin types that have Json representation to Swagger/Json types.
//
// See https://golang.org/pkg/builtin/ and http://swagger.io/specification/
func SwaggerSchemaForType(typeName string, prop ifaces.SwaggerTypable) error {
switch typeName {
Expand All @@ -37,9 +38,9 @@ func SwaggerSchemaForType(typeName string, prop ifaces.SwaggerTypable) error {
case "complex128", "complex64":
return fmt.Errorf("unsupported builtin %q (no JSON marshaller): %w", typeName, ErrResolver)
case "error":
// TODO: error is often marshalled into a string but not always (e.g. errors package creates
// Proposal for enhancement: error is often marshalled into a string but not always (e.g. errors package creates
// errors that are marshalled into an empty object), this could be handled the same way
// custom JSON marshallers are handled (in future)
// custom JSON marshallers are handled (future)
prop.Typed("string", "")
case "float32":
prop.Typed("number", "float")
Expand Down
287 changes: 287 additions & 0 deletions internal/builders/resolvers/resolvers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@
package resolvers

import (
"errors"
"go/ast"
"go/token"
"go/types"
"testing"

"github.com/go-openapi/codescan/internal/ifaces"
"github.com/go-openapi/codescan/internal/scantest/mocks"
oaispec "github.com/go-openapi/spec"
"github.com/go-openapi/testify/v2/assert"
"github.com/go-openapi/testify/v2/require"
Expand Down Expand Up @@ -35,3 +41,284 @@ func TestAddExtension(t *testing.T) {
AddExtension(ve, key3, value3, true)
assert.Nil(t, ve.Extensions[key3])
}

// typableCapture builds a MockSwaggerTypable whose Typed() records
// the (swaggerType, format) pair it receives.
func typableCapture() (*mocks.MockSwaggerTypable, *[2]string) {
got := new([2]string)
m := &mocks.MockSwaggerTypable{
TypedFunc: func(swaggerType, format string) {
got[0] = swaggerType
got[1] = format
},
}
return m, got
}

func TestSwaggerSchemaForType(t *testing.T) {
t.Run("supported builtins map to swagger types", func(t *testing.T) {
cases := []struct {
in string
want [2]string
}{
{"bool", [2]string{"boolean", ""}},
{"byte", [2]string{"integer", "uint8"}},
{"error", [2]string{"string", ""}},
{"float32", [2]string{"number", "float"}},
{"float64", [2]string{"number", "double"}},
{"int", [2]string{"integer", "int64"}},
{"int8", [2]string{"integer", "int8"}},
{"int16", [2]string{"integer", "int16"}},
{"int32", [2]string{"integer", "int32"}},
{"int64", [2]string{"integer", "int64"}},
{"rune", [2]string{"integer", "int32"}},
{"string", [2]string{"string", ""}},
{"uint", [2]string{"integer", "uint64"}},
{"uint8", [2]string{"integer", "uint8"}},
{"uint16", [2]string{"integer", "uint16"}},
{"uint32", [2]string{"integer", "uint32"}},
{"uint64", [2]string{"integer", "uint64"}},
{"uintptr", [2]string{"integer", "uint64"}},
{"object", [2]string{"object", ""}},
}

for _, tc := range cases {
t.Run(tc.in, func(t *testing.T) {
m, got := typableCapture()
require.NoError(t, SwaggerSchemaForType(tc.in, m))
assert.EqualT(t, tc.want, *got)
})
}
})

t.Run("complex64/128 returns ErrResolver", func(t *testing.T) {
for _, name := range []string{"complex64", "complex128"} {
t.Run(name, func(t *testing.T) {
m := &mocks.MockSwaggerTypable{
TypedFunc: func(_, _ string) { t.Fatalf("Typed should not be called for %q", name) },
}
err := SwaggerSchemaForType(name, m)
require.Error(t, err)
require.TrueT(t, errors.Is(err, ErrResolver))
})
}
})

t.Run("unknown type returns ErrResolver", func(t *testing.T) {
m := &mocks.MockSwaggerTypable{
TypedFunc: func(_, _ string) { t.Fatal("Typed should not be called for unknown type") },
}
err := SwaggerSchemaForType("bogus", m)
require.Error(t, err)
require.TrueT(t, errors.Is(err, ErrResolver))
})
}

func TestUnsupportedBuiltin(t *testing.T) {
t.Run("nil Obj returns false", func(t *testing.T) {
m := &mocks.MockObjecter{ObjFunc: func() *types.TypeName { return nil }}
assert.FalseT(t, UnsupportedBuiltin(m))
})

t.Run("unsafe package returns true", func(t *testing.T) {
pkg := types.NewPackage("unsafe", "unsafe")
tn := types.NewTypeName(token.NoPos, pkg, "Pointer", nil)
m := &mocks.MockObjecter{ObjFunc: func() *types.TypeName { return tn }}
assert.TrueT(t, UnsupportedBuiltin(m))
})

t.Run("user package returns false", func(t *testing.T) {
pkg := types.NewPackage("example.com/foo", "foo")
tn := types.NewTypeName(token.NoPos, pkg, "Bar", nil)
m := &mocks.MockObjecter{ObjFunc: func() *types.TypeName { return tn }}
assert.FalseT(t, UnsupportedBuiltin(m))
})

t.Run("builtin complex64 returns true", func(t *testing.T) {
tn := types.NewTypeName(token.NoPos, nil, "complex64", nil)
m := &mocks.MockObjecter{ObjFunc: func() *types.TypeName { return tn }}
assert.TrueT(t, UnsupportedBuiltin(m))
})

t.Run("builtin int returns false", func(t *testing.T) {
tn := types.NewTypeName(token.NoPos, nil, "int", nil)
m := &mocks.MockObjecter{ObjFunc: func() *types.TypeName { return tn }}
assert.FalseT(t, UnsupportedBuiltin(m))
})
}

func TestUnsupportedBuiltinType(t *testing.T) {
t.Run("Basic complex64 returns true", func(t *testing.T) {
assert.TrueT(t, UnsupportedBuiltinType(types.Typ[types.Complex64]))
})

t.Run("Basic int returns false", func(t *testing.T) {
assert.FalseT(t, UnsupportedBuiltinType(types.Typ[types.Int]))
})

t.Run("UnsafePointer Basic returns true", func(t *testing.T) {
assert.TrueT(t, UnsupportedBuiltinType(types.Typ[types.UnsafePointer]))
})

t.Run("Chan returns true", func(t *testing.T) {
ch := types.NewChan(types.SendRecv, types.Typ[types.Int])
assert.TrueT(t, UnsupportedBuiltinType(ch))
})

t.Run("Signature returns true", func(t *testing.T) {
sig := types.NewSignatureType(nil, nil, nil, types.NewTuple(), types.NewTuple(), false)
assert.TrueT(t, UnsupportedBuiltinType(sig))
})

t.Run("TypeParam returns true", func(t *testing.T) {
pkg := types.NewPackage("example.com/foo", "foo")
tn := types.NewTypeName(token.NoPos, pkg, "T", nil)
tp := types.NewTypeParam(tn, types.NewInterfaceType(nil, nil))
assert.TrueT(t, UnsupportedBuiltinType(tp))
})

t.Run("Named delegates to UnsupportedBuiltin", func(t *testing.T) {
pkg := types.NewPackage("example.com/foo", "foo")
tn := types.NewTypeName(token.NoPos, pkg, "Foo", nil)
named := types.NewNamed(tn, types.Typ[types.Int], nil)
assert.FalseT(t, UnsupportedBuiltinType(named))
})

t.Run("Map (default case) returns false", func(t *testing.T) {
m := types.NewMap(types.Typ[types.String], types.Typ[types.Int])
assert.FalseT(t, UnsupportedBuiltinType(m))
})
}

func TestIsFieldStringable(t *testing.T) {
t.Run("scalar Ident returns true", func(t *testing.T) {
for _, name := range []string{"int", "int8", "int64", "uint32", "float64", "string", "bool"} {
assert.TrueT(t, IsFieldStringable(ast.NewIdent(name)), "want true for %s", name)
}
})

t.Run("non-scalar Ident returns false", func(t *testing.T) {
for _, name := range []string{"any", "Foo", "float32"} { // float32 isn't in the stringable set
assert.FalseT(t, IsFieldStringable(ast.NewIdent(name)), "want false for %s", name)
}
})

t.Run("StarExpr to scalar returns true", func(t *testing.T) {
star := &ast.StarExpr{X: ast.NewIdent("int")}
assert.TrueT(t, IsFieldStringable(star))
})

t.Run("StarExpr to non-scalar returns false", func(t *testing.T) {
star := &ast.StarExpr{X: ast.NewIdent("Foo")}
assert.FalseT(t, IsFieldStringable(star))
})

t.Run("other AST expr returns false", func(t *testing.T) {
// SelectorExpr like pkg.Type — neither Ident nor StarExpr.
sel := &ast.SelectorExpr{X: ast.NewIdent("pkg"), Sel: ast.NewIdent("Type")}
assert.FalseT(t, IsFieldStringable(sel))

// ArrayType — same, hits the else-return-false branch.
arr := &ast.ArrayType{Elt: ast.NewIdent("int")}
assert.FalseT(t, IsFieldStringable(arr))
})
}

func TestParseJSONTag(t *testing.T) {
ident := func(name string) *ast.Field {
return &ast.Field{
Names: []*ast.Ident{ast.NewIdent(name)},
Type: ast.NewIdent("int"),
}
}

t.Run("no tag uses field name", func(t *testing.T) {
f := ident("Foo")
name, ignore, isString, omitEmpty, err := ParseJSONTag(f)
require.NoError(t, err)
assert.EqualT(t, "Foo", name)
assert.FalseT(t, ignore)
assert.FalseT(t, isString)
assert.FalseT(t, omitEmpty)
})

t.Run("json tag renames", func(t *testing.T) {
f := ident("Foo")
f.Tag = &ast.BasicLit{Value: "`json:\"foo,omitempty\"`"}
name, ignore, isString, omitEmpty, err := ParseJSONTag(f)
require.NoError(t, err)
assert.EqualT(t, "foo", name)
assert.FalseT(t, ignore)
assert.FalseT(t, isString)
assert.TrueT(t, omitEmpty)
})

t.Run("json:\"-\" marks ignored", func(t *testing.T) {
f := ident("Foo")
f.Tag = &ast.BasicLit{Value: "`json:\"-\"`"}
name, ignore, _, _, err := ParseJSONTag(f)
require.NoError(t, err)
assert.EqualT(t, "Foo", name)
assert.TrueT(t, ignore)
})

t.Run("json:\",string\" on scalar sets isString", func(t *testing.T) {
f := ident("Foo")
f.Tag = &ast.BasicLit{Value: "`json:\",string\"`"}
name, _, isString, _, err := ParseJSONTag(f)
require.NoError(t, err)
assert.EqualT(t, "Foo", name)
assert.TrueT(t, isString)
})

t.Run("whitespace-only tag value falls through", func(t *testing.T) {
f := ident("Foo")
// Backticks wrap a single space — TrimSpace of `" "` != empty (outer check
// passes), but Unquote yields " " which does TrimSpace to empty — hits
// the final fallthrough.
f.Tag = &ast.BasicLit{Value: "` `"}
name, ignore, isString, omitEmpty, err := ParseJSONTag(f)
require.NoError(t, err)
assert.EqualT(t, "Foo", name)
assert.FalseT(t, ignore)
assert.FalseT(t, isString)
assert.FalseT(t, omitEmpty)
})

t.Run("malformed tag returns Unquote error", func(t *testing.T) {
f := ident("Foo")
// Unquote requires surrounding backticks/quotes. Bare word is invalid.
f.Tag = &ast.BasicLit{Value: "not-a-quoted-tag"}
_, _, _, _, err := ParseJSONTag(f)
require.Error(t, err)
})
}

func TestMustNotBeABuiltinType(t *testing.T) {
t.Run("user type does not panic", func(t *testing.T) {
pkg := types.NewPackage("example.com/foo", "foo")
tn := types.NewTypeName(token.NoPos, pkg, "Foo", nil)
assert.NotPanics(t, func() { MustNotBeABuiltinType(tn) })
})

t.Run("builtin panics wrapping ErrInternal", func(t *testing.T) {
tn := types.NewTypeName(token.NoPos, nil, "complex64", nil)
defer func() {
r := recover()
require.NotNil(t, r)
err, ok := r.(error)
require.TrueT(t, ok, "panic value should be an error")
require.TrueT(t, errors.Is(err, ErrInternal))
}()
MustNotBeABuiltinType(tn)
})
}

func TestInternalError_Error(t *testing.T) {
// Exercises the internalError.Error method that satisfies the `error`
// interface used in fmt.Errorf("...%w", ErrInternal).
assert.EqualT(t, string(ErrInternal), ErrInternal.Error())
}

// Assert at compile time that our typable fixture satisfies the interface.
var _ ifaces.SwaggerTypable = (*mocks.MockSwaggerTypable)(nil)
8 changes: 0 additions & 8 deletions internal/builders/responses/typable.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,6 @@ func (ht responseTypable) Schema() *oaispec.Schema {
return ht.response.Schema
}

func (ht responseTypable) SetSchema(schema *oaispec.Schema) {
ht.response.Schema = schema
}

func (ht responseTypable) CollectionOf(items *oaispec.Items, format string) {
ht.header.CollectionOf(items, format)
}

func (ht responseTypable) AddExtension(key string, value any) {
ht.response.AddExtension(key, value)
}
Expand Down
Loading
Loading