Skip to content

Commit

Permalink
Allow local reference resolution for model and field definitions
Browse files Browse the repository at this point in the history
  • Loading branch information
stefannegele committed Nov 21, 2023
1 parent 7e33744 commit b143248
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 33 deletions.
49 changes: 36 additions & 13 deletions model.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package datacontract

import (
"errors"
"fmt"
"log"
"strings"
)

// internal model
Expand Down Expand Up @@ -577,24 +579,24 @@ func GetModelsFromSpecification(contract DataContract, pathToModels []string) (*
return nil, nil
}

modelsMap, err := fieldAsMap(specModels, pathToModels)
modelsMap, err := fieldAsMapWithReferenceResolution(specModels, pathToModels, contract, 0)
if err != nil {
return nil, err
}

return internalModelSpecification(modelsMap)
return internalModelSpecification(modelsMap, contract)
}

func internalModelSpecification(modelsMap map[string]any) (*InternalModelSpecification, error) {
func internalModelSpecification(modelsMap map[string]any, contract DataContract) (*InternalModelSpecification, error) {
var internalModels []InternalModel

for modelName, specModel := range modelsMap {
specModelMap, err := fieldAsMap(specModel, []string{modelName})
specModelMap, err := fieldAsMapWithReferenceResolution(specModel, []string{modelName}, contract, 0)
if err != nil {
return nil, err
}

model, err := internalModel(specModelMap, modelName)
model, err := internalModel(specModelMap, modelName, contract)
if err != nil {
return nil, err
}
Expand All @@ -605,10 +607,10 @@ func internalModelSpecification(modelsMap map[string]any) (*InternalModelSpecifi
return &InternalModelSpecification{Type: "data-contract-specification", Models: internalModels}, nil
}

func internalModel(specModelMap map[string]any, modelName string) (*InternalModel, error) {
func internalModel(specModelMap map[string]any, modelName string, contract DataContract) (*InternalModel, error) {
modelType, err := fieldAsString(specModelMap, "type")
modelDescription, err := fieldAsString(specModelMap, "description")
internalFields, err := internalFields(specModelMap)
internalFields, err := internalFields(specModelMap, contract)

if err != nil {
return nil, err
Expand All @@ -622,18 +624,18 @@ func internalModel(specModelMap map[string]any, modelName string) (*InternalMode
}, nil
}

func internalFields(specModelMap map[string]any) ([]InternalField, error) {
func internalFields(specModelMap map[string]any, contract DataContract) ([]InternalField, error) {
var internalFields []InternalField

fields := specModelMap["fields"]
if fields != nil {
fieldsMap, err := fieldAsMap(fields, []string{"fields"})
fieldsMap, err := fieldAsMapWithReferenceResolution(fields, []string{"fields"}, contract, 0)
if err != nil {
return nil, err
}

for fieldName, specField := range fieldsMap {
fieldMap, err := fieldAsMap(specField, []string{fieldName})
fieldMap, err := fieldAsMapWithReferenceResolution(specField, []string{fieldName}, contract, 0)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -664,14 +666,35 @@ func internalField(fieldMap map[string]any, fieldName string) (*InternalField, e
}, nil
}

func fieldAsMap(field any, fieldPath []string) (map[string]any, error) {
if result, ok := field.(map[string]any); !ok {
func fieldAsMapWithReferenceResolution(field any, fieldPath []string, contract DataContract, referenceCount int) (map[string]any, error) {
if anyMap, isMap := field.(map[string]any); !isMap {
return nil, fmt.Errorf("field %v is not a map", fieldPath)
} else if reference, isReference := anyMap["$ref"].(string); isReference {
return resolveReferencedMap(reference, contract, referenceCount)
} else {
return result, nil
return anyMap, nil
}
}

// todo merge this into GetValue
func resolveReferencedMap(reference string, contract DataContract, referenceCount int) (map[string]any, error) {
if referenceCount >= 50 {
return nil, errors.New("reference maximum reached, seems like references are circular")
}

if strings.HasPrefix(reference, "#") {
localReferencePath := strings.Split(reference, "/")[1:]
resolved, err := GetValue(contract, localReferencePath)
if err != nil {
return nil, err
}

return fieldAsMapWithReferenceResolution(resolved, localReferencePath, contract, referenceCount+1)
}

return nil, nil
}

func fieldAsString(anyMap map[string]any, fieldName string) (*string, error) {
field := anyMap[fieldName]

Expand Down
96 changes: 76 additions & 20 deletions model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,36 @@ func TestGetSpecModelSpecification(t *testing.T) {
fieldType := "int"
fieldDescription := "my field description"

fieldDefinition := map[string]any{
"type": fieldType,
"description": fieldDescription,
}
modelDefinition := map[string]any{
"description": modelDescription,
"type": modelType,
"fields": map[string]any{
fieldName: fieldDefinition,
},
}

expected := InternalModelSpecification{
Type: "data-contract-specification",
Models: []InternalModel{
{
Name: modelName,
Type: &modelType,
Description: &modelDescription,
Fields: []InternalField{
{
Name: fieldName,
Type: &fieldType,
Description: &fieldDescription,
},
},
},
},
}

type args struct {
contract DataContract
pathToModels []string
Expand All @@ -345,6 +375,37 @@ func TestGetSpecModelSpecification(t *testing.T) {
}{
{
name: "simple model",
args: args{
contract: DataContract{
"models": map[string]any{
modelName: modelDefinition,
},
},
pathToModels: []string{"models"},
},
want: &expected,
wantErr: false,
},
{
name: "with definitions reference on model",
args: args{
contract: DataContract{
"models": map[string]any{
modelName: map[string]any{
"$ref": fmt.Sprintf("#/definitions/%v", modelName),
},
},
"definitions": map[string]any{
modelName: modelDefinition,
},
},
pathToModels: []string{"models"},
},
want: &expected,
wantErr: false,
},
{
name: "with definitions reference on field",
args: args{
contract: DataContract{
"models": map[string]any{
Expand All @@ -353,36 +414,31 @@ func TestGetSpecModelSpecification(t *testing.T) {
"type": modelType,
"fields": map[string]any{
fieldName: map[string]any{
"type": fieldType,
"description": fieldDescription,
"$ref": fmt.Sprintf("#/definitions/%v", fieldName),
},
},
},
},
},
pathToModels: []string{"models"},
},
want: &InternalModelSpecification{
Type: "data-contract-specification",
Models: []InternalModel{
{
Name: modelName,
Type: &modelType,
Description: &modelDescription,
Fields: []InternalField{
{
Name: fieldName,
Type: &fieldType,
Description: &fieldDescription,
},
},
"definitions": map[string]any{
fieldName: fieldDefinition,
},
},
pathToModels: []string{"models"},
},
want: &expected,
wantErr: false,
},
//{
// name: "with definition resolution",
// name: "with local file reference on model",
//},
//{
// name: "with local file reference on field",
//},
//{
// name: "with remote reference on model",
//},
//{
// name: "with remote reference on field",
//},
}
for _, tt := range tests {
Expand Down

0 comments on commit b143248

Please sign in to comment.