Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for enums w/o needing to manually specify variations #97

Merged
merged 1 commit into from
Dec 11, 2024
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
77 changes: 63 additions & 14 deletions docparse/jsonschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ const (
paramOmitEmpty = "omitempty"
paramReadOnly = "readonly"
paramOmitDoc = "omitdoc"
paramEnum = "enum"
)

func setTags(name, fName string, p *Schema, tags []string) error {
Expand All @@ -129,6 +130,9 @@ func setTags(name, fName string, p *Schema, tags []string) error {
case paramReadOnly:
t := true
p.Readonly = &t
case paramEnum:
// For this type of enum, we figure out the variations based on the type.
p.Type = "enum"

// Various string formats.
// https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-7.3
Expand Down Expand Up @@ -274,7 +278,18 @@ start:

// Simple identifiers such as "string", "int", "MyType", etc.
case *ast.Ident:

mappedType, mappedFormat := MapType(prog, pkg+"."+typ.Name)
if mappedType != "" {
p.Type = JSONSchemaType(mappedType)
}
if p.Type == "enum" && len(p.Enum) == 0 {
if variations, err := getEnumVariations(ref.File, pkg, typ.Name); len(variations) > 0 {
p.Enum = variations
} else if err != nil {
return nil, err
}
}
if mappedType == "" {
// Only check for canonicalType if this isn't mapped.
canon, err := canonicalType(ref.File, pkg, typ)
Expand All @@ -285,10 +300,6 @@ start:
sw = canon
goto start
}
}
if mappedType != "" {
p.Type = JSONSchemaType(mappedType)
} else {
p.Type = JSONSchemaType(typ.Name)
}
if mappedFormat != "" {
Expand Down Expand Up @@ -360,8 +371,9 @@ start:

switch resolvType := ts.Type.(type) {
case *ast.ArrayType:
isEnum := p.Type == "enum"
p.Type = "array"
err := resolveArray(prog, ref, pkg, &p, resolvType.Elt)
err := resolveArray(prog, ref, pkg, &p, resolvType.Elt, isEnum)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -403,9 +415,10 @@ start:

// Array and slices.
case *ast.ArrayType:
isEnum := p.Type == "enum"
p.Type = "array"

err := resolveArray(prog, ref, pkg, &p, typ.Elt)
err := resolveArray(prog, ref, pkg, &p, typ.Elt, isEnum)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -443,6 +456,37 @@ start:
return &p, nil
}

// Helper function to extract enum variations from a file.
func getEnumVariations(currentFile, pkgPath, typeName string) ([]string, error) {
resolvedPath, pkg, err := resolvePackage(currentFile, pkgPath)
if err != nil {
return nil, fmt.Errorf("could not resolve package: %v", err)
}
decls, err := getDecls(pkg, resolvedPath)
if err != nil {
return nil, err
}
var variations []string
for _, decl := range decls {
if decl.vs == nil {
continue
}
if exprToString(decl.vs.Type) != typeName {
continue
}
if len(decl.vs.Names) == 0 || len(decl.vs.Values) == 0 {
continue
}
// All enums variations are required to have the type as their prefix.
if !strings.HasPrefix(exprToString(decl.vs.Names[0]), typeName) {
continue
}
variations = append(variations, exprToString(decl.vs.Values[0]))
}

return variations, nil
}

func dropTypePointers(typ ast.Expr) ast.Expr {
var t *ast.StarExpr
var ok bool
Expand Down Expand Up @@ -488,7 +532,7 @@ func lookupTypeAndRef(file, pkg, name string) (string, string, error) {
return t, sRef, nil
}

func resolveArray(prog *Program, ref Reference, pkg string, p *Schema, typ ast.Expr) error {
func resolveArray(prog *Program, ref Reference, pkg string, p *Schema, typ ast.Expr, isEnum bool) error {
asw := typ

var name *ast.Ident
Expand All @@ -509,7 +553,7 @@ arrayStart:
p.Items = &Schema{Type: JSONSchemaType(typ.Name)}

// Generally an item is an enum rather than the array itself
if p.Enum != nil {
if len(p.Enum) > 0 {
p.Items.Enum = p.Enum
p.Enum = nil
}
Expand Down Expand Up @@ -562,14 +606,19 @@ arrayStart:
if err != nil {
return err
}
if t != "" {
if isPrimitive(t) {
if p.Items == nil {
p.Items = &Schema{}
if t != "" && isPrimitive(t) {
if p.Items == nil {
p.Items = &Schema{}
}
p.Items.Type = t
if isEnum && len(p.Items.Enum) == 0 {
if variations, err := getEnumVariations(ref.File, pkg, name.Name); len(variations) > 0 {
p.Items.Enum = variations
} else if err != nil {
return err
}
p.Items.Type = t
return nil
}
return nil
}

sRef := lookup
Expand Down
2 changes: 2 additions & 0 deletions docparse/jsonschema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ func TestFieldToProperty(t *testing.T) {
"sliceP": {Type: "array", Items: &Schema{Type: "string"}},
"cstr": {Type: "string"},
"cstrP": {Type: "string"},
"enumStr": {Type: "string", Enum: []string{"a", "b", "c"}},
"enumsStr": {Type: "array", Items: &Schema{Type: "string", Enum: []string{"a", "b", "c"}}},
"bar": {Reference: "a.bar"},
"barP": {Reference: "a.bar"},
"pkg": {Reference: "mail.Address"},
Expand Down
34 changes: 23 additions & 11 deletions docparse/testdata/src/a/a.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,21 @@ import "net/mail"
type foo struct {
// Documented str field.
// Newline.
str string
byt []byte
r rune
b bool // Inline docs.
fl float64
err error
strP *string
slice []string
sliceP []*string
cstr customStr
cstrP *customStr
str string
byt []byte
r rune
b bool // Inline docs.
fl float64
err error
strP *string
slice []string
sliceP []*string
cstr customStr
cstrP *customStr
// {enum}
enumStr customStr
// {enum}
enumsStr []customStr
bar bar
barP *bar
pkg mail.Address
Expand All @@ -41,8 +45,16 @@ type nested struct {
deeper refAnother
}

type customStrs []customStr

type customStr string

const (
customStrA customStr = "a"
customStrB customStr = "b"
customStrC customStr = "c"
)

// Document me bar!
type bar struct {
str string
Expand Down
Loading