Skip to content

Commit fdf230d

Browse files
committed
feat(api): refactor to generate parents/patterns/children
The previous version of the API struct required that pointers to parents and children were also passed - this isn't strictly possible with value serialization from Json / Yaml, resulting in significantly complexity around deduplication of objects. In addition, there were default values in loaded resources missing (such as path). To resolve this, many of the api.Resource fields were turned semi-private, generated on load, loading from an API associated with the resources. By lazy-generating these values, it enables a minimal set of values to be specified in markup formats, while still enabling these structs to be used in the code base itself. Ultimately, this prevents the need to define an on-disk resource format and an in-memory format.
1 parent 91ff0e0 commit fdf230d

File tree

14 files changed

+182
-163
lines changed

14 files changed

+182
-163
lines changed

examples/resource-definitions/bookstore.yaml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ resources:
2626
book: &book
2727
singular: "book"
2828
plural: "books"
29-
parents:
30-
- *publisher
29+
parents: ["publisher"]
3130
schema:
3231
type: object
3332
required: ["author", "edition", "isbn", "price", "published"]
@@ -59,7 +58,8 @@ resources:
5958
type: string
6059
x-aep-field-number: 2
6160
methods:
62-
create: {}
61+
create:
62+
supports_user_settable_create: true
6363
read: {}
6464
update: {}
6565
delete: {}
@@ -80,8 +80,7 @@ resources:
8080
book-edition:
8181
singular: "book-edition"
8282
plural: "book-editions"
83-
parents:
84-
- *book
83+
parents: ["book"]
8584
schema:
8685
type: object
8786
required: ["displayname"]

pkg/api/api.go

Lines changed: 15 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package api
22

33
import (
4-
"encoding/json"
54
"fmt"
65
"log/slog"
76
"strings"
@@ -29,19 +28,6 @@ type Contact struct {
2928
URL string
3029
}
3130

32-
func LoadAPIFromJson(data []byte) (*API, error) {
33-
api := &API{}
34-
err := json.Unmarshal(data, api)
35-
if err != nil {
36-
return nil, fmt.Errorf("error unmarshalling API: %v", err)
37-
}
38-
err = addImplicitFields(api)
39-
if err != nil {
40-
return nil, fmt.Errorf("error adding defaults to API: %v", err)
41-
}
42-
return api, nil
43-
}
44-
4531
func GetAPI(api *openapi.OpenAPI, serverURL, pathPrefix string) (*API, error) {
4632
if api.OASVersion() == "" {
4733
return nil, fmt.Errorf("unable to detect OAS version. Please add a openapi field or a swagger field")
@@ -350,23 +336,26 @@ func getOrPopulateResource(singular string, pattern []string, s *openapi.Schema,
350336
parents = append(parents, parentResource)
351337
parentResource.Children = append(parentResource.Children, r)
352338
}
339+
patternElems := strings.Split(strings.TrimPrefix(s.XAEPResource.Patterns[0], "/"), "/")
353340
r = &Resource{
354-
Singular: s.XAEPResource.Singular,
355-
Plural: s.XAEPResource.Plural,
356-
Parents: parents,
357-
Children: []*Resource{},
358-
PatternElems: strings.Split(strings.TrimPrefix(s.XAEPResource.Patterns[0], "/"), "/"),
359-
Schema: s,
341+
Singular: s.XAEPResource.Singular,
342+
Plural: s.XAEPResource.Plural,
343+
Parents: s.XAEPResource.Parents,
344+
parentResources: parents,
345+
Children: []*Resource{},
346+
patternElems: patternElems,
347+
Schema: s,
360348
}
361349
} else {
362350
// best effort otherwise
363351
r = &Resource{
364-
Schema: s,
365-
PatternElems: pattern,
366-
Singular: singular,
367-
Parents: []*Resource{},
368-
Children: []*Resource{},
369-
Plural: plural(singular),
352+
Schema: s,
353+
patternElems: pattern,
354+
Singular: singular,
355+
Parents: []string{},
356+
parentResources: []*Resource{},
357+
Children: []*Resource{},
358+
Plural: plural(singular),
370359
}
371360
}
372361
resourceBySingular[singular] = r
@@ -408,19 +397,3 @@ func getContact(contact openapi.Contact) *Contact {
408397
func plural(singular string) string {
409398
return singular + "s"
410399
}
411-
412-
// addImplicitFields adds implicit fields to the API object,
413-
// such as the "path" variable in the resource.
414-
func addImplicitFields(api *API) error {
415-
// add the path variable to the resource
416-
for _, r := range api.Resources {
417-
if r.Schema.Properties != nil {
418-
r.Schema.Properties[constants.FIELD_PATH_NAME] = openapi.Schema{
419-
Type: "string",
420-
Description: "The server-assigned path of the resource, which is unique within the service.",
421-
XAEPFieldNumber: constants.FIELD_PATH_NUMBER,
422-
}
423-
}
424-
}
425-
return nil
426-
}

pkg/api/api_test.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ func TestGetAPI(t *testing.T) {
180180

181181
widget, ok := sd.Resources["widget"]
182182
assert.True(t, ok, "widget resource should exist")
183-
assert.Equal(t, widget.PatternElems, []string{"widgets", "{widget}"})
183+
assert.Equal(t, widget.PatternElems(), []string{"widgets", "{widget}"})
184184
assert.Equal(t, sd.ServerURL, "https://api.example.com")
185185
assert.NotNil(t, widget.Methods.Get, "should have GET method")
186186
assert.NotNil(t, widget.Methods.List, "should have LIST method")
@@ -251,7 +251,7 @@ func TestGetAPI(t *testing.T) {
251251
assert.True(t, ok, "widget resource should exist")
252252
assert.Equal(t, "widget", widget.Singular)
253253
assert.Equal(t, "widgets", widget.Plural)
254-
assert.Equal(t, []string{"widgets", "{widget}"}, widget.PatternElems)
254+
assert.Equal(t, []string{"widgets", "{widget}"}, widget.PatternElems())
255255
},
256256
},
257257
{
@@ -333,7 +333,7 @@ func TestGetAPI(t *testing.T) {
333333
widget, ok := sd.Resources["widget"]
334334
assert.True(t, ok, "widget resource should exist")
335335
assert.NotNil(t, widget.Methods.Get, "should have GET method")
336-
assert.Equal(t, []string{"widgets", "{widget}"}, widget.PatternElems)
336+
assert.Equal(t, []string{"widgets", "{widget}"}, widget.PatternElems())
337337
},
338338
},
339339
{
@@ -574,7 +574,7 @@ func TestGetAPI(t *testing.T) {
574574
}
575575
}
576576

577-
func TestParseBookstoreYAMLDirectly(t *testing.T) {
577+
func TestLoadFromJsonBookstore(t *testing.T) {
578578
// Construct the path relative to the test file's location
579579
yamlPath := filepath.Join("..", "..", "examples", "resource-definitions", "bookstore.yaml")
580580

@@ -587,6 +587,7 @@ func TestParseBookstoreYAMLDirectly(t *testing.T) {
587587
require.NoError(t, err, "Failed to convert YAML to JSON")
588588

589589
apiResult, err := LoadAPIFromJson(jsonData)
590+
require.NoError(t, err, "Failed to load API from JSON")
590591
err = json.Unmarshal(jsonData, &apiResult)
591592
require.NoError(t, err, "Failed to unmarshal JSON into api.API struct")
592593

@@ -627,6 +628,7 @@ func TestParseBookstoreYAMLDirectly(t *testing.T) {
627628
assert.Equal(t, "array", bookResource.Schema.Properties["isbn"].Type)
628629
assert.NotNil(t, bookResource.Methods.List, "'book' should have List method")
629630
assert.True(t, bookResource.Methods.List.HasUnreachableResources)
631+
assert.True(t, bookResource.Methods.Create.SupportsUserSettableCreate)
630632
assert.Len(t, bookResource.CustomMethods, 1, "'book' should have 1 custom method")
631633
assert.Equal(t, "archive", bookResource.CustomMethods[0].Name)
632634
}

pkg/api/loader.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package api
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
"github.com/aep-dev/aep-lib-go/pkg/constants"
8+
"github.com/aep-dev/aep-lib-go/pkg/openapi"
9+
)
10+
11+
func LoadAPIFromJson(data []byte) (*API, error) {
12+
api := &API{}
13+
err := json.Unmarshal(data, api)
14+
if err != nil {
15+
return nil, fmt.Errorf("error unmarshalling API: %v", err)
16+
}
17+
err = addImplicitFields(api)
18+
if err != nil {
19+
return nil, fmt.Errorf("error adding defaults to API: %v", err)
20+
}
21+
return api, nil
22+
}
23+
24+
// addImplicitFields adds implicit fields to the API object,
25+
// such as the "path" variable in the resource.
26+
func addImplicitFields(api *API) error {
27+
// add the path variable to the resource
28+
for _, r := range api.Resources {
29+
r.API = api
30+
if r.Schema.Properties == nil {
31+
r.Schema.Properties = make(map[string]openapi.Schema)
32+
}
33+
r.Schema.Properties[constants.FIELD_PATH_NAME] = openapi.Schema{
34+
Type: "string",
35+
Description: "The server-assigned path of the resource, which is unique within the service.",
36+
XAEPFieldNumber: constants.FIELD_PATH_NUMBER,
37+
}
38+
for _, p := range r.Parents {
39+
if parent, ok := api.Resources[p]; ok {
40+
r.parentResources = append(r.parentResources, parent)
41+
parent.Children = append(parent.Children, r)
42+
} else {
43+
return fmt.Errorf("parent resource %s not found for resource %s", p, r.Singular)
44+
}
45+
}
46+
}
47+
return nil
48+
}

pkg/api/openapi.go

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -411,15 +411,11 @@ func ConvertToOpenAPI(api *API) (*openapi.OpenAPI, error) {
411411
addMethodToPath(paths, cmPath, methodType, methodInfo)
412412
}
413413
}
414-
parents := []string{}
415-
for _, p := range r.Parents {
416-
parents = append(parents, p.Singular)
417-
}
418414
d.XAEPResource = &openapi.XAEPResource{
419415
Singular: r.Singular,
420416
Plural: r.Plural,
421417
Patterns: patterns,
422-
Parents: parents,
418+
Parents: r.Parents,
423419
}
424420
components.Schemas[r.Singular] = *d
425421
}
@@ -480,11 +476,11 @@ type PathWithParams struct {
480476
func generateParentPatternsWithParams(r *Resource) (string, *[]PathWithParams) {
481477
// case 1: pattern elems are present, so we use them.
482478
// TODO(yft): support multiple patterns
483-
if len(r.PatternElems) > 0 {
484-
collection := fmt.Sprintf("/%s", r.PatternElems[len(r.PatternElems)-2])
479+
if len(r.patternElems) > 0 {
480+
collection := fmt.Sprintf("/%s", r.patternElems[len(r.patternElems)-2])
485481
params := []openapi.Parameter{}
486-
for i := 0; i < len(r.PatternElems)-2; i += 2 {
487-
pElem := r.PatternElems[i+1]
482+
for i := 0; i < len(r.patternElems)-2; i += 2 {
483+
pElem := r.patternElems[i+1]
488484
params = append(params, openapi.Parameter{
489485
In: "path",
490486
Name: pElem[1 : len(pElem)-1],
@@ -494,7 +490,7 @@ func generateParentPatternsWithParams(r *Resource) (string, *[]PathWithParams) {
494490
},
495491
})
496492
}
497-
pattern := strings.Join(r.PatternElems[0:len(r.PatternElems)-2], "/")
493+
pattern := strings.Join(r.patternElems[0:len(r.patternElems)-2], "/")
498494
if pattern != "" {
499495
pattern = fmt.Sprintf("/%s", pattern)
500496
}
@@ -505,7 +501,7 @@ func generateParentPatternsWithParams(r *Resource) (string, *[]PathWithParams) {
505501
// case 2: no pattern elems, so we need to generate the collection names
506502
collection := fmt.Sprintf("/%s", CollectionName(r))
507503
pwps := []PathWithParams{}
508-
for _, parent := range r.Parents {
504+
for _, parent := range r.ParentResources() {
509505
singular := parent.Singular
510506
basePattern := fmt.Sprintf("/%s/{%s}", CollectionName(parent), singular)
511507
baseParam := openapi.Parameter{
@@ -519,7 +515,7 @@ func generateParentPatternsWithParams(r *Resource) (string, *[]PathWithParams) {
519515
Resource: singular,
520516
},
521517
}
522-
if len(parent.Parents) == 0 {
518+
if len(parent.ParentResources()) == 0 {
523519
pwps = append(pwps, PathWithParams{
524520
Pattern: basePattern,
525521
Params: []openapi.Parameter{baseParam},

pkg/api/openapi_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ func TestGenerateParentPatternsWithParams(t *testing.T) {
305305
{
306306
name: "with pattern elements",
307307
resource: &Resource{
308-
PatternElems: []string{"databases", "{database}", "tables", "{table}"},
308+
patternElems: []string{"databases", "{database}", "tables", "{table}"},
309309
Singular: "table",
310310
},
311311
wantCollection: "/tables",
@@ -331,7 +331,7 @@ func TestGenerateParentPatternsWithParams(t *testing.T) {
331331
{
332332
name: "with pattern elements no nesting",
333333
resource: &Resource{
334-
PatternElems: []string{"databases", "{database}"},
334+
patternElems: []string{"databases", "{database}"},
335335
Singular: "database",
336336
},
337337
wantCollection: "/databases",
@@ -348,7 +348,7 @@ func TestGenerateParentPatternsWithParams(t *testing.T) {
348348
resource: &Resource{
349349
Singular: "table",
350350
Plural: "tables",
351-
Parents: []*Resource{
351+
parentResources: []*Resource{
352352
{
353353
Singular: "database",
354354
Plural: "databases",
@@ -380,11 +380,11 @@ func TestGenerateParentPatternsWithParams(t *testing.T) {
380380
resource: &Resource{
381381
Singular: "table",
382382
Plural: "tables",
383-
Parents: []*Resource{
383+
parentResources: []*Resource{
384384
{
385385
Singular: "database",
386386
Plural: "databases",
387-
Parents: []*Resource{
387+
parentResources: []*Resource{
388388
{
389389
Singular: "account",
390390
Plural: "accounts",

0 commit comments

Comments
 (0)