From 9c05fe11d4a5b9be67aa3c1c0f399a0865ffa9eb Mon Sep 17 00:00:00 2001 From: Pablo Andres Fuente Date: Mon, 29 Dec 2025 14:00:41 -0300 Subject: [PATCH] Fixing anyOf/oneOf without schema $ref properties and Link titles with parentheses The code assumed that anyOf and oneOf arrays always contain schemas with $ref properties. Schemas like this one... ```json "anyOf": [ { "required": ["cloud", "suite"], "additionalProperties": true }, { "required": ["cloud", "tags"], "additionalProperties": true } ] ``` ...caused this error: "runtime error: invalid memory address or nil pointer dereference" To fix it I added nil checks Also if a Link title used parentheses the generated Go code was invalid. To fix it I added parentheses to the regex used to split Link titles. I also add it go.mod --- gen.go | 4 +- gen_test.go | 166 ++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 3 + helpers.go | 11 +++- helpers_test.go | 16 +++++ 5 files changed, 195 insertions(+), 5 deletions(-) create mode 100644 go.mod diff --git a/gen.go b/gen.go index d6824d5..91c5bd9 100644 --- a/gen.go +++ b/gen.go @@ -102,9 +102,9 @@ func (s *Schema) Resolve(r *Schema, rs ResolvedSet) *Schema { for { if s.Ref != nil { s = s.Ref.Resolve(r) - } else if len(s.OneOf) > 0 { + } else if len(s.OneOf) > 0 && s.OneOf[0].Ref != nil { s = s.OneOf[0].Ref.Resolve(r) - } else if len(s.AnyOf) > 0 { + } else if len(s.AnyOf) > 0 && s.AnyOf[0].Ref != nil { s = s.AnyOf[0].Ref.Resolve(r) } else { break diff --git a/gen_test.go b/gen_test.go index 288df29..cf4ebd7 100644 --- a/gen_test.go +++ b/gen_test.go @@ -81,6 +81,81 @@ var generateTests = []struct { }, }, }, + { + ExpectedServiceFunctions: []string{"ResourceCreate", "ResourceInfoSpecial"}, + Schema: &Schema{ + Title: "Bug Reproducer API", + Properties: map[string]*Schema{ + "resource": { + Ref: NewReference("#/definitions/resource"), + }, + }, + Definitions: map[string]*Schema{ + "resource": { + Title: "Resource", + Type: "object", + Definitions: map[string]*Schema{ + "id": { + Type: "string", + }, + "name": { + Type: "string", + }, + }, + Links: []*Link{ + { + Title: "Create", + Rel: "create", + HRef: NewHRef("/resources"), + Method: "POST", + Schema: &Schema{ + Type: "object", + Properties: map[string]*Schema{ + "name": { + Ref: NewReference("#/definitions/resource/definitions/name"), + }, + }, + AnyOf: []Schema{ + { + Required: []string{"name"}, + }, + { + Required: []string{"option"}, + }, + }, + }, + TargetSchema: &Schema{ + Ref: NewReference("#/definitions/resource"), + }, + }, + { + Title: "Info (Special)", + Rel: "self", + HRef: NewHRef("/resources/{(%23%2Fdefinitions%2Fresource%2Fdefinitions%2Fid)}"), + Method: "GET", + TargetSchema: &Schema{ + Ref: NewReference("#/definitions/resource"), + }, + }, + }, + Properties: map[string]*Schema{ + "id": { + Ref: NewReference("#/definitions/resource/definitions/id"), + }, + "name": { + Ref: NewReference("#/definitions/resource/definitions/name"), + }, + }, + }, + }, + Links: []*Link{ + { + Rel: "self", + HRef: NewHRef("https://api.example.com"), + }, + }, + }, + }, } func TestGenerate(t *testing.T) { @@ -194,6 +269,97 @@ var resolveTests = []struct { }, }, }, + { + Schema: &Schema{ + Title: "AnyOfWithoutRef", + Type: "object", + Definitions: map[string]*Schema{ + "resource": { + Title: "Resource", + Type: "object", + Links: []*Link{ + { + Title: "Create", + Rel: "create", + HRef: NewHRef("/resources"), + Method: "POST", + Schema: &Schema{ + Type: "object", + Properties: map[string]*Schema{ + "name": { + Type: "string", + }, + }, + AnyOf: []Schema{ + { + Required: []string{"name"}, + }, + { + Required: []string{"option"}, + }, + }, + }, + }, + }, + Definitions: map[string]*Schema{ + "id": { + Type: "string", + }, + }, + Properties: map[string]*Schema{ + "id": { + Ref: NewReference("#/definitions/resource/definitions/id"), + }, + }, + }, + }, + Properties: map[string]*Schema{ + "resource": { + Ref: NewReference("#/definitions/resource"), + }, + }, + }, + }, + { + Schema: &Schema{ + Title: "OneOfWithoutRef", + Type: "object", + Definitions: map[string]*Schema{ + "resource": { + Title: "Resource", + Type: "object", + Links: []*Link{ + { + Title: "Update", + Rel: "update", + HRef: NewHRef("/resources"), + Method: "PATCH", + Schema: &Schema{ + Type: "object", + OneOf: []Schema{ + { + Properties: map[string]*Schema{ + "name": {Type: "string"}, + }, + }, + { + Properties: map[string]*Schema{ + "value": {Type: "integer"}, + }, + }, + }, + }, + }, + }, + }, + }, + Properties: map[string]*Schema{ + "resource": { + Ref: NewReference("#/definitions/resource"), + }, + }, + }, + }, } func TestResolve(t *testing.T) { diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7ee6c97 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/interagent/schematic + +go 1.25.1 diff --git a/helpers.go b/helpers.go index c3ddd9f..e5ada2e 100644 --- a/helpers.go +++ b/helpers.go @@ -32,7 +32,7 @@ var helpers = template.FuncMap{ var ( newlines = regexp.MustCompile(`(?m:\s*$)`) acronyms = regexp.MustCompile(`(Url|Http|Id|Io|Ip|Ike|Uuid|Api|Uri|Ssl|Cname|Oauth|Otp|Cidr|Nat|Vpn)$`) - camelcase = regexp.MustCompile(`(?m)[-.$/:_{}\s]+`) + camelcase = regexp.MustCompile(`(?m)[-.$/:_{}()\s]+`) ) func goType(p *Schema) string { @@ -108,18 +108,23 @@ func initialLow(ident string) string { func depunct(ident string, initialCap bool) string { matches := camelcase.Split(ident, -1) + var result []string for i, m := range matches { + if m == "" { + continue + } if initialCap || i > 0 { m = capFirst(m) } - matches[i] = acronyms.ReplaceAllStringFunc(m, func(c string) string { + var replaced = acronyms.ReplaceAllStringFunc(m, func(c string) string { if len(c) > 4 { return strings.ToUpper(c[:2]) + c[2:] } return strings.ToUpper(c) }) + result = append(result, replaced) } - return strings.Join(matches, "") + return strings.Join(result, "") } func capFirst(ident string) string { diff --git a/helpers_test.go b/helpers_test.go index d583e36..b375c6d 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -26,6 +26,22 @@ var initialCapTests = []struct { In: "Dyno all", Out: "DynoAll", }, + { + In: "Info (Consecutive)", + Out: "InfoConsecutive", + }, + { + In: "Get (Special)", + Out: "GetSpecial", + }, + { + In: "List (All Items)", + Out: "ListAllItems", + }, + { + In: "Update(Force)", + Out: "UpdateForce", + }, } func TestInitialCap(t *testing.T) {