From 6d194b698d366088e0f20a665fb2dbcdbf197722 Mon Sep 17 00:00:00 2001 From: Maxim Panfilov Date: Thu, 26 Sep 2024 19:31:10 +0300 Subject: [PATCH] zero lint! --- .gitignore | 3 +- .golangci.yml | 12 +- complex.go | 361 +++++++++++++++++++++--------------- consts.go | 9 + example.go | 105 ++++++----- fragment.go | 113 +++++++----- node.go | 210 +++++++++++---------- scalars.go | 44 ++--- shape.go | 363 +++++++++++++++++++++---------------- unwrap.go | 493 +++++++++++++++++++++++++++++--------------------- validate.go | 272 +++++++++++++++------------- 11 files changed, 1142 insertions(+), 843 deletions(-) diff --git a/.gitignore b/.gitignore index b591f30..bddd854 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ *.tokens cover.html cover.out -.build \ No newline at end of file +.build +/.idea diff --git a/.golangci.yml b/.golangci.yml index 1ffff77..a90b070 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -83,7 +83,7 @@ linters-settings: gocognit: # Minimal code complexity to report. # Default: 30 (but we recommend 10-20) - min-complexity: 20 + min-complexity: 30 gocritic: # Settings passed to gocritic. @@ -235,17 +235,14 @@ linters: - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 - exhaustive # checks exhaustiveness of enum switch statements - fatcontext # detects nested contexts in loops - #- forbidigo # forbids identifiers - funlen # tool for detection of long functions - gocheckcompilerdirectives # validates go compiler directive comments (//go:) - - gochecknoglobals # checks that no global variables exist - gochecknoinits # checks that no init functions are present in Go code - gochecksumtype # checks exhaustiveness on Go "sum types" - gocognit # computes and checks the cognitive complexity of functions - goconst # finds repeated strings that could be replaced by a constant - gocritic # provides diagnostics that check for bugs, performance and style issues - gocyclo # computes and checks the cyclomatic complexity of functions - #- godot # checks if comments end in a period - goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod - gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations @@ -256,7 +253,6 @@ linters: - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) - makezero # finds slice declarations with non-zero initial length - mirror # reports wrong mirror patterns of bytes/strings usage - #- mnd # detects magic numbers - musttag # enforces field tags in (un)marshaled structs - nakedret # finds naked returns in functions greater than a specified function length - nestif # reports deeply nested if statements @@ -280,7 +276,6 @@ linters: - tenv # detects using os.Setenv instead of t.Setenv since Go1.17 - testableexamples # checks if examples are testable (have an expected output) - testifylint # checks usage of github.com/stretchr/testify - #- testpackage # makes you use a separate _test package - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes - unconvert # removes unnecessary type conversions - unparam # reports unused function parameters @@ -314,7 +309,10 @@ linters: #- errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted #- execinquery # [deprecated] checks query string in Query function which reads your Go src files and warning it finds #- exportloopref # [not necessary from Go 1.22] checks for pointers to enclosing loop variables + #- forbidigo # forbids identifiers #- forcetypeassert # [replaced by errcheck] finds forced type assertions + #- gochecknoglobals # checks that no global variables exist + #- godot # checks if comments end in a period #- gofmt # [replaced by goimports] checks whether code was gofmt-ed #- gofumpt # [replaced by goimports, gofumports is not available yet] checks whether code was gofumpt-ed #- gosmopolitan # reports certain i18n/l10n anti-patterns in your Go codebase @@ -322,9 +320,11 @@ linters: #- importas # enforces consistent import aliases #- maintidx # measures the maintainability index of each function #- misspell # [useless] finds commonly misspelled English words in comments + #- mnd # detects magic numbers #- nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity #- paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test #- tagliatelle # checks the struct tags + #- testpackage # makes you use a separate _test package #- thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers #- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines diff --git a/complex.go b/complex.go index 934b299..f683e61 100644 --- a/complex.go +++ b/complex.go @@ -203,6 +203,40 @@ type ObjectShape struct { ObjectFacets } +func (s *ObjectShape) unmarshalPatternProperties( + nodeName, propertyName string, data *yaml.Node, hasImplicitOptional bool) error { + if s.PatternProperties == nil { + s.PatternProperties = orderedmap.New[string, PatternProperty]() + } + property, err := s.raml.makePatternProperty(nodeName, propertyName, data, s.Location, + hasImplicitOptional) + if err != nil { + return StacktraceNewWrapped("make pattern property", err, s.Location, + WithNodePosition(data)) + } + s.PatternProperties.Set(propertyName, property) + s.raml.PutShapePtr(property.Shape) + return nil +} + +func (s *ObjectShape) unmarshalProperty(nodeName string, data *yaml.Node) error { + propertyName, hasImplicitOptional := s.raml.chompImplicitOptional(nodeName) + if len(propertyName) > 1 && propertyName[0] == '/' && propertyName[len(propertyName)-1] == '/' { + return s.unmarshalPatternProperties(nodeName, propertyName, data, hasImplicitOptional) + } + + if s.Properties == nil { + s.Properties = orderedmap.New[string, Property]() + } + property, err := s.raml.makeProperty(nodeName, propertyName, data, s.Location, hasImplicitOptional) + if err != nil { + return StacktraceNewWrapped("make property", err, s.Location, WithNodePosition(data)) + } + s.Properties.Set(property.Name, property) + s.raml.PutShapePtr(property.Shape) + return nil +} + // UnmarshalYAMLNodes unmarshals the object shape from YAML nodes. func (s *ObjectShape) unmarshalYAMLNodes(v []*yaml.Node) error { for i := 0; i != len(v); i += 2 { @@ -245,29 +279,8 @@ func (s *ObjectShape) unmarshalYAMLNodes(v []*yaml.Node) error { nodeName := valueNode.Content[j].Value data := valueNode.Content[j+1] - propertyName, hasImplicitOptional := s.raml.chompImplicitOptional(nodeName) - if len(propertyName) > 1 && propertyName[0] == '/' && propertyName[len(propertyName)-1] == '/' { - if s.PatternProperties == nil { - s.PatternProperties = orderedmap.New[string, PatternProperty]() - } - property, err := s.raml.makePatternProperty(nodeName, propertyName, data, s.Location, - hasImplicitOptional) - if err != nil { - return StacktraceNewWrapped("make pattern property", err, s.Location, - WithNodePosition(data)) - } - s.PatternProperties.Set(propertyName, property) - s.raml.PutShapePtr(property.Shape) - } else { - if s.Properties == nil { - s.Properties = orderedmap.New[string, Property]() - } - property, err := s.raml.makeProperty(nodeName, propertyName, data, s.Location, hasImplicitOptional) - if err != nil { - return StacktraceNewWrapped("make property", err, s.Location, WithNodePosition(data)) - } - s.Properties.Set(property.Name, property) - s.raml.PutShapePtr(property.Shape) + if err := s.unmarshalProperty(nodeName, data); err != nil { + return fmt.Errorf("unmarshal property: %w", err) } } default: @@ -321,21 +334,9 @@ func (s *ObjectShape) clone(history []Shape) Shape { return ptr } -func (s *ObjectShape) Validate(v interface{}, ctxPath string) error { - i, ok := v.(map[string]interface{}) - if !ok { - return fmt.Errorf("invalid type, got %T, expected map[string]interface{}", v) - } - - mapLen := uint64(len(i)) - if s.MinProperties != nil && mapLen < *s.MinProperties { - return fmt.Errorf("object must have at least %d properties", *s.MinProperties) - } - if s.MaxProperties != nil && mapLen > *s.MaxProperties { - return fmt.Errorf("object must have not more than %d properties", *s.MaxProperties) - } +func (s *ObjectShape) validateProperties(ctxPath string, props map[string]interface{}) error { restrictedAdditionalProperties := s.AdditionalProperties != nil && !*s.AdditionalProperties - for k, item := range i { + for k, item := range props { // Explicitly defined properties have priority over pattern properties. ctxPathK := ctxPath + "." + k if s.Properties != nil { @@ -371,80 +372,101 @@ func (s *ObjectShape) Validate(v interface{}, ctxPath string) error { return fmt.Errorf("unexpected additional property \"%s\"", k) } } - return nil } -// Inherit merges the source shape into the target shape. -func (s *ObjectShape) Inherit(source Shape) (Shape, error) { - ss, ok := source.(*ObjectShape) +func (s *ObjectShape) Validate(v interface{}, ctxPath string) error { + props, ok := v.(map[string]interface{}) if !ok { - return nil, stacktrace.New("cannot inherit from different type", s.Location, - stacktrace.WithPosition(&s.Position), - stacktrace.WithInfo("source", source.Base().Type), - stacktrace.WithInfo("target", s.Base().Type)) + return fmt.Errorf("invalid type, got %T, expected map[string]interface{}", v) } - // Discriminator and AdditionalProperties are inherited as is - if s.AdditionalProperties == nil { - s.AdditionalProperties = ss.AdditionalProperties + if err := s.validateProperties(ctxPath, props); err != nil { + return fmt.Errorf("validate properties: %w", err) } - if s.Discriminator == nil { - s.Discriminator = ss.Discriminator + + mapLen := uint64(len(props)) + if s.MinProperties != nil && mapLen < *s.MinProperties { + return fmt.Errorf("object must have at least %d properties", *s.MinProperties) } + if s.MaxProperties != nil && mapLen > *s.MaxProperties { + return fmt.Errorf("object must have not more than %d properties", *s.MaxProperties) + } + + return nil +} +func (s *ObjectShape) inheritMinProperties(source *ObjectShape) error { if s.MinProperties == nil { - s.MinProperties = ss.MinProperties - } else if ss.MinProperties != nil && *s.MinProperties < *ss.MinProperties { - return nil, stacktrace.New("minProperties constraint violation", s.Location, + s.MinProperties = source.MinProperties + } else if source.MinProperties != nil && *s.MinProperties > *source.MinProperties { + return stacktrace.New("minProperties constraint violation", s.Location, stacktrace.WithPosition(&s.Position), - stacktrace.WithInfo("source", *ss.MinProperties), + stacktrace.WithInfo("source", *source.MinProperties), stacktrace.WithInfo("target", *s.MinProperties)) } + return nil +} + +func (s *ObjectShape) inheritMaxProperties(source *ObjectShape) error { if s.MaxProperties == nil { - s.MaxProperties = ss.MaxProperties - } else if ss.MaxProperties != nil && *s.MaxProperties > *ss.MaxProperties { - return nil, stacktrace.New("maxProperties constraint violation", s.Location, + s.MaxProperties = source.MaxProperties + } else if source.MaxProperties != nil && *s.MaxProperties < *source.MaxProperties { + return stacktrace.New("maxProperties constraint violation", s.Location, stacktrace.WithPosition(&s.Position), - stacktrace.WithInfo("source", *ss.MaxProperties), + stacktrace.WithInfo("source", *source.MaxProperties), stacktrace.WithInfo("target", *s.MaxProperties)) } + return nil +} +func (s *ObjectShape) inheritProperties(source *ObjectShape) error { if s.Properties == nil { - s.Properties = ss.Properties - } else if ss.Properties != nil { - for pair := ss.Properties.Oldest(); pair != nil; pair = pair.Next() { - k, sourceProp := pair.Key, pair.Value - if targetProp, present := s.Properties.Get(k); present { - if sourceProp.Required && !targetProp.Required { - return nil, stacktrace.New("cannot make required property optional", s.Location, - stacktrace.WithPosition(&(*targetProp.Shape).Base().Position), - stacktrace.WithInfo("property", k), - stacktrace.WithInfo("source", sourceProp.Required), - stacktrace.WithInfo("target", targetProp.Required), - stacktrace.WithType(stacktrace.TypeUnwrapping)) - } - _, err := s.raml.Inherit(*sourceProp.Shape, *targetProp.Shape) - if err != nil { - return nil, StacktraceNewWrapped("inherit property", err, s.Base().Location, - stacktrace.WithPosition(&(*targetProp.Shape).Base().Position), - stacktrace.WithInfo("property", k), - stacktrace.WithType(stacktrace.TypeUnwrapping)) - } - } else { - s.Properties.Set(k, sourceProp) + s.Properties = source.Properties + return nil + } + + if source.Properties == nil { + return nil + } + + for pair := source.Properties.Oldest(); pair != nil; pair = pair.Next() { + k, sourceProp := pair.Key, pair.Value + if targetProp, present := s.Properties.Get(k); present { + if sourceProp.Required && !targetProp.Required { + return stacktrace.New("cannot make required property optional", s.Location, + stacktrace.WithPosition(&(*targetProp.Shape).Base().Position), + stacktrace.WithInfo("property", k), + stacktrace.WithInfo("source", sourceProp.Required), + stacktrace.WithInfo("target", targetProp.Required), + stacktrace.WithType(stacktrace.TypeUnwrapping)) } + _, err := s.raml.Inherit(*sourceProp.Shape, *targetProp.Shape) + if err != nil { + return StacktraceNewWrapped("inherit property", err, s.Location, + stacktrace.WithPosition(&(*targetProp.Shape).Base().Position), + stacktrace.WithInfo("property", k), + stacktrace.WithType(stacktrace.TypeUnwrapping)) + } + } else { + s.Properties.Set(k, sourceProp) } } + return nil +} + +func (s *ObjectShape) inheritPatternProperties(source *ObjectShape) error { if s.PatternProperties == nil { - s.PatternProperties = ss.PatternProperties - } else if ss.PatternProperties != nil { - for pair := ss.PatternProperties.Oldest(); pair != nil; pair = pair.Next() { + s.PatternProperties = source.PatternProperties + return nil + } + if source.PatternProperties != nil { + for pair := source.PatternProperties.Oldest(); pair != nil; pair = pair.Next() { k, sourceProp := pair.Key, pair.Value if targetProp, present := s.PatternProperties.Get(k); present { _, err := s.raml.Inherit(*sourceProp.Shape, *targetProp.Shape) if err != nil { - return nil, StacktraceNewWrapped("inherit pattern property", err, s.Base().Location, + return StacktraceNewWrapped("inherit pattern property", err, s.Location, stacktrace.WithPosition(&(*targetProp.Shape).Base().Position), stacktrace.WithInfo("property", k), stacktrace.WithType(stacktrace.TypeUnwrapping)) @@ -454,63 +476,119 @@ func (s *ObjectShape) Inherit(source Shape) (Shape, error) { } } } + return nil +} + +// Inherit merges the source shape into the target shape. +func (s *ObjectShape) Inherit(source Shape) (Shape, error) { + ss, ok := source.(*ObjectShape) + if !ok { + return nil, stacktrace.New("cannot inherit from different type", s.Location, + stacktrace.WithPosition(&s.Position), + stacktrace.WithInfo("source", source.Base().Type), + stacktrace.WithInfo("target", s.Base().Type)) + } + + // Discriminator and AdditionalProperties are inherited as is + if s.AdditionalProperties == nil { + s.AdditionalProperties = ss.AdditionalProperties + } + if s.Discriminator == nil { + s.Discriminator = ss.Discriminator + } + + if err := s.inheritMinProperties(ss); err != nil { + return nil, fmt.Errorf("inherit minProperties: %w", err) + } + + if err := s.inheritMaxProperties(ss); err != nil { + return nil, fmt.Errorf("inherit maxProperties: %w", err) + } + + if err := s.inheritProperties(ss); err != nil { + return nil, fmt.Errorf("inherit properties: %w", err) + } + + if err := s.inheritPatternProperties(ss); err != nil { + return nil, fmt.Errorf("inherit pattern properties: %w", err) + } + return s, nil } -func (s *ObjectShape) Check() error { - if s.MinProperties != nil && s.MaxProperties != nil && *s.MinProperties > *s.MaxProperties { - return stacktrace.New("minProperties must be less than or equal to maxProperties", +func (s *ObjectShape) checkPatternProperties() error { + if s.PatternProperties == nil { + return nil + } + if s.AdditionalProperties != nil && !*s.AdditionalProperties { + // TODO: We actually can allow pattern properties with "additionalProperties: false" for stricter + // validation. + // This will contradict RAML 1.0 spec, but JSON Schema allows that. + // https://json-schema.org/understanding-json-schema/reference/object#additionalproperties + return stacktrace.New("pattern properties are not allowed with \"additionalProperties: false\"", s.Location, stacktrace.WithPosition(&s.Position)) } - if s.PatternProperties != nil { - if s.AdditionalProperties != nil && !*s.AdditionalProperties { - // TODO: We actually can allow pattern properties with "additionalProperties: false" for stricter - // validation. - // This will contradict RAML 1.0 spec, but JSON Schema allows that. - // https://json-schema.org/understanding-json-schema/reference/object#additionalproperties - return stacktrace.New("pattern properties are not allowed with \"additionalProperties: false\"", - s.Location, stacktrace.WithPosition(&s.Position)) + for pair := s.PatternProperties.Oldest(); pair != nil; pair = pair.Next() { + prop := pair.Value + if err := (*prop.Shape).Check(); err != nil { + return StacktraceNewWrapped("check pattern property", err, s.Location, + stacktrace.WithPosition(&(*prop.Shape).Base().Position), + stacktrace.WithInfo("property", prop.Pattern.String())) } - for pair := s.PatternProperties.Oldest(); pair != nil; pair = pair.Next() { - prop := pair.Value - if err := (*prop.Shape).Check(); err != nil { - return StacktraceNewWrapped("check pattern property", err, s.Location, - stacktrace.WithPosition(&(*prop.Shape).Base().Position), - stacktrace.WithInfo("property", prop.Pattern.String())) - } + } + return nil +} + +func (s *ObjectShape) checkProperties() error { + if s.Properties == nil { + return nil + } + + for pair := s.Properties.Oldest(); pair != nil; pair = pair.Next() { + prop := pair.Value + if err := (*prop.Shape).Check(); err != nil { + return StacktraceNewWrapped("check property", err, s.Location, + stacktrace.WithPosition(&(*prop.Shape).Base().Position), + stacktrace.WithInfo("property", prop.Name)) } } - if s.Properties != nil { - for pair := s.Properties.Oldest(); pair != nil; pair = pair.Next() { - prop := pair.Value - if err := (*prop.Shape).Check(); err != nil { - return StacktraceNewWrapped("check property", err, s.Location, - stacktrace.WithPosition(&(*prop.Shape).Base().Position), - stacktrace.WithInfo("property", prop.Name)) - } + // FIXME: Need to validate on which level the discriminator is applied to avoid potential false positives. + // Inline definitions with discriminator are not allowed. + // TODO: Setting discriminator should be allowed only on scalar shapes. + if s.Discriminator != nil { + prop, ok := s.Properties.Get(*s.Discriminator) + if !ok { + return stacktrace.New("discriminator property not found", s.Location, + stacktrace.WithPosition(&s.Position), + stacktrace.WithInfo("discriminator", *s.Discriminator)) } - // FIXME: Need to validate on which level the discriminator is applied to avoid potential false positives. - // Inline definitions with discriminator are not allowed. - // TODO: Setting discriminator should be allowed only on scalar shapes. - if s.Discriminator != nil { - prop, ok := s.Properties.Get(*s.Discriminator) - if !ok { - return stacktrace.New("discriminator property not found", s.Location, - stacktrace.WithPosition(&s.Position), - stacktrace.WithInfo("discriminator", *s.Discriminator)) - } - discriminatorValue := s.DiscriminatorValue - if discriminatorValue == nil { - discriminatorValue = s.Base().Name - } - ps := *prop.Shape - if err := ps.Validate(discriminatorValue, "$"); err != nil { - return StacktraceNewWrapped("validate discriminator value", err, s.Location, - stacktrace.WithPosition(&s.Base().Position), - stacktrace.WithInfo("discriminator", *s.Discriminator)) - } + discriminatorValue := s.DiscriminatorValue + if discriminatorValue == nil { + discriminatorValue = s.Base().Name } - } else if s.Discriminator != nil { + ps := *prop.Shape + if err := ps.Validate(discriminatorValue, "$"); err != nil { + return StacktraceNewWrapped("validate discriminator value", err, s.Location, + stacktrace.WithPosition(&s.Base().Position), + stacktrace.WithInfo("discriminator", *s.Discriminator)) + } + } + + return nil +} + +func (s *ObjectShape) Check() error { + if s.MinProperties != nil && s.MaxProperties != nil && *s.MinProperties > *s.MaxProperties { + return stacktrace.New("minProperties must be less than or equal to maxProperties", + s.Location, stacktrace.WithPosition(&s.Position)) + } + if err := s.checkPatternProperties(); err != nil { + return fmt.Errorf("check pattern properties: %w", err) + } + if err := s.checkProperties(); err != nil { + return fmt.Errorf("check properties: %w", err) + } + if s.Discriminator != nil && s.Properties == nil { return stacktrace.New("discriminator without properties", s.Location, stacktrace.WithPosition(&s.Position)) } @@ -609,7 +687,7 @@ type UnionShape struct { } // UnmarshalYAMLNodes unmarshals the union shape from YAML nodes. -func (s *UnionShape) unmarshalYAMLNodes(v []*yaml.Node) error { +func (s *UnionShape) unmarshalYAMLNodes(_ []*yaml.Node) error { return nil } @@ -720,16 +798,16 @@ func (s *JSONShape) Clone() Shape { return &c } -func (s *JSONShape) clone(history []Shape) Shape { +func (s *JSONShape) clone(_ []Shape) Shape { return s.Clone() } -func (s *JSONShape) Validate(v interface{}, ctxPath string) error { +func (s *JSONShape) Validate(_ interface{}, _ string) error { // TODO: Implement validation with JSON Schema return nil } -func (s *JSONShape) unmarshalYAMLNodes(v []*yaml.Node) error { +func (s *JSONShape) unmarshalYAMLNodes(_ []*yaml.Node) error { return nil } @@ -769,11 +847,11 @@ func (s *UnknownShape) Clone() Shape { return &c } -func (s *UnknownShape) clone(history []Shape) Shape { +func (s *UnknownShape) clone(_ []Shape) Shape { return s.Clone() } -func (s *UnknownShape) Validate(v interface{}, ctxPath string) error { +func (s *UnknownShape) Validate(_ interface{}, _ string) error { return fmt.Errorf("cannot validate against unknown shape") } @@ -782,7 +860,7 @@ func (s *UnknownShape) unmarshalYAMLNodes(v []*yaml.Node) error { return nil } -func (s *UnknownShape) Inherit(source Shape) (Shape, error) { +func (s *UnknownShape) Inherit(_ Shape) (Shape, error) { return nil, stacktrace.New("cannot inherit from unknown shape", s.Location, stacktrace.WithPosition(&s.Position)) } @@ -796,7 +874,7 @@ type RecursiveShape struct { Head *Shape } -func (s *RecursiveShape) unmarshalYAMLNodes(v []*yaml.Node) error { +func (s *RecursiveShape) unmarshalYAMLNodes(_ []*yaml.Node) error { return nil } @@ -810,7 +888,7 @@ func (s *RecursiveShape) Clone() Shape { return &c } -func (s *RecursiveShape) clone(history []Shape) Shape { +func (s *RecursiveShape) clone(_ []Shape) Shape { return s.Clone() } @@ -821,7 +899,8 @@ func (s *RecursiveShape) Validate(v interface{}, ctxPath string) error { return nil } -func (s *RecursiveShape) Inherit(source Shape) (Shape, error) { +// Inherit merges the source shape into the target shape. +func (s *RecursiveShape) Inherit(_ Shape) (Shape, error) { return nil, stacktrace.New("cannot inherit from recursive shape", s.Location, stacktrace.WithPosition(&s.Position)) } diff --git a/consts.go b/consts.go index 317709e..fb85d81 100644 --- a/consts.go +++ b/consts.go @@ -1,41 +1,50 @@ package raml +// SetOfScalarTypes contains a set of scalar types var SetOfScalarTypes = map[string]struct{}{ TypeString: {}, TypeInteger: {}, TypeNumber: {}, TypeBoolean: {}, TypeDatetime: {}, TypeDatetimeOnly: {}, TypeDateOnly: {}, TypeTimeOnly: {}, TypeFile: {}, } +// SetOfStringFacets contains a set of string facets var SetOfStringFacets = map[string]struct{}{ FacetMinLength: {}, FacetMaxLength: {}, FacetPattern: {}, } +// SetOfNumberFacets contains a set of number facets var SetOfNumberFacets = map[string]struct{}{ FacetMinimum: {}, FacetMaximum: {}, FacetMultipleOf: {}, } +// SetOfFileFacets contains a set of file facets var SetOfFileFacets = map[string]struct{}{ FacetFileTypes: {}, } +// SetOfObjectFacets contains a set of object facets var SetOfObjectFacets = map[string]struct{}{ FacetProperties: {}, FacetAdditionalProperties: {}, FacetMinProperties: {}, FacetMaxProperties: {}, FacetDiscriminator: {}, FacetDiscriminatorValue: {}, } +// SetOfArrayFacets contains a set of array facets var SetOfArrayFacets = map[string]struct{}{ FacetItems: {}, FacetMinItems: {}, FacetMaxItems: {}, FacetUniqueItems: {}, } +// SetOfNumberFormats contains a set of number formats var SetOfNumberFormats = map[string]struct{}{ "float": {}, "double": {}, } +// SetOfIntegerFormats contains a set of integer formats var SetOfIntegerFormats = map[string]int8{ // int is an alias for int32 // long is an alias for int64 "int8": 0, "int16": 1, "int32": 2, "int": 2, "int64": 3, "long": 3, } +// SetOfDateTimeFormats contains a set of date-time formats var SetOfDateTimeFormats = map[string]struct{}{ "rfc3339": {}, "rfc2616": {}, } diff --git a/example.go b/example.go index 7685f97..499092d 100644 --- a/example.go +++ b/example.go @@ -1,12 +1,71 @@ package raml import ( + "fmt" + orderedmap "github.com/wk8/go-ordered-map/v2" "gopkg.in/yaml.v3" "github.com/acronis/go-stacktrace" ) +func (ex *Example) decode(node *yaml.Node, valueNode *yaml.Node, location string) error { + switch node.Value { + case "strict": + if err := valueNode.Decode(&ex.Strict); err != nil { + return StacktraceNewWrapped("decode strict", err, location, WithNodePosition(valueNode)) + } + case "displayName": + if err := valueNode.Decode(&ex.DisplayName); err != nil { + return StacktraceNewWrapped("decode displayName", err, location, WithNodePosition(valueNode)) + } + case "description": + if err := valueNode.Decode(&ex.Description); err != nil { + return StacktraceNewWrapped("decode description", err, location, WithNodePosition(valueNode)) + } + default: + if IsCustomDomainExtensionNode(node.Value) { + deName, de, err := ex.raml.unmarshalCustomDomainExtension(location, node, valueNode) + if err != nil { + return StacktraceNewWrapped("unmarshal custom domain extension", err, location, WithNodePosition(valueNode)) + } + ex.CustomDomainProperties.Set(deName, de) + } + } + return nil +} + +func (ex *Example) fill(location string, value *yaml.Node) error { + var valueKey *yaml.Node + // First lookup for the "value" key. + for i := 0; i != len(value.Content); i += 2 { + node := value.Content[i] + valueNode := value.Content[i+1] + if node.Value == "value" { + valueKey = valueNode + break + } + } + // If "value" key is found, then the example is considered as a map with additional properties + if valueKey != nil { + for i := 0; i != len(value.Content); i += 2 { + node := value.Content[i] + valueNode := value.Content[i+1] + if err := ex.decode(node, valueNode, location); err != nil { + return fmt.Errorf("decode example: %w", err) + } + } + n, err := ex.raml.makeRootNode(valueKey, location) + if err != nil { + return StacktraceNewWrapped("make node", err, location, WithNodePosition(valueKey)) + } + ex.Data = n + return nil + } + + return nil +} + // makeExample creates an example from the given value node func (r *RAML) makeExample(value *yaml.Node, name string, location string) (*Example, error) { ex := &Example{ @@ -21,50 +80,8 @@ func (r *RAML) makeExample(value *yaml.Node, name string, location string) (*Exa // 1. A value with an example of ObjectShape. // 2. A map with the required "value" key that contains the actual example and additional properties of Example. if value.Kind == yaml.MappingNode { - var valueKey *yaml.Node - // First lookup for the "value" key. - for i := 0; i != len(value.Content); i += 2 { - node := value.Content[i] - valueNode := value.Content[i+1] - if node.Value == "value" { - valueKey = valueNode - break - } - } - // If "value" key is found, then the example is considered as a map with additional properties - if valueKey != nil { - for i := 0; i != len(value.Content); i += 2 { - node := value.Content[i] - valueNode := value.Content[i+1] - switch node.Value { - case "strict": - if err := valueNode.Decode(&ex.Strict); err != nil { - return nil, StacktraceNewWrapped("decode strict", err, location, WithNodePosition(valueNode)) - } - case "displayName": - if err := valueNode.Decode(&ex.DisplayName); err != nil { - return nil, StacktraceNewWrapped("decode displayName", err, location, WithNodePosition(valueNode)) - } - case "description": - if err := valueNode.Decode(&ex.Description); err != nil { - return nil, StacktraceNewWrapped("decode description", err, location, WithNodePosition(valueNode)) - } - default: - if IsCustomDomainExtensionNode(node.Value) { - deName, de, err := r.unmarshalCustomDomainExtension(location, node, valueNode) - if err != nil { - return nil, StacktraceNewWrapped("unmarshal custom domain extension", err, location, WithNodePosition(valueNode)) - } - ex.CustomDomainProperties.Set(deName, de) - } - } - } - n, err := r.makeRootNode(valueKey, location) - if err != nil { - return nil, StacktraceNewWrapped("make node", err, location, WithNodePosition(valueKey)) - } - ex.Data = n - return ex, nil + if err := ex.fill(location, value); err != nil { + return nil, fmt.Errorf("fill example from mapping node: %w", err) } } // In all other cases, the example is considered as a value node diff --git a/fragment.go b/fragment.go index ada1880..bd926fa 100644 --- a/fragment.go +++ b/fragment.go @@ -1,6 +1,7 @@ package raml import ( + "fmt" "path/filepath" orderedmap "github.com/wk8/go-ordered-map/v2" @@ -56,6 +57,66 @@ type LibraryLink struct { stacktrace.Position } +func (l *Library) unmarshalUses(valueNode *yaml.Node) { + if valueNode.Tag == TagNull { + return + } + + l.Uses = orderedmap.New[string, *LibraryLink](len(valueNode.Content) / 2) + // Map nodes come in pairs in order [key, value] + for j := 0; j != len(valueNode.Content); j += 2 { + name := valueNode.Content[j].Value + path := valueNode.Content[j+1] + l.Uses.Set(name, &LibraryLink{ + Value: path.Value, + Location: l.Location, + Position: stacktrace.Position{Line: path.Line, Column: path.Column}, + }) + } +} + +func (l *Library) unmarshalTypes(valueNode *yaml.Node) { + if valueNode.Tag == TagNull { + return + } + + l.Types = orderedmap.New[string, *Shape](len(valueNode.Content) / 2) + // Map nodes come in pairs in order [key, value] + for j := 0; j != len(valueNode.Content); j += 2 { + name := valueNode.Content[j].Value + data := valueNode.Content[j+1] + shape, err := l.raml.makeShape(data, name, l.Location) + if err != nil { + panic(StacktraceNewWrapped("parse types: make shape", err, l.Location, WithNodePosition(data))) + } + l.Types.Set(name, shape) + l.raml.PutTypeIntoFragment(name, l.Location, shape) + l.raml.PutShapePtr(shape) + } +} + +func (l *Library) unmarshalAnnotationTypes(valueNode *yaml.Node) error { + if valueNode.Tag == TagNull { + return nil + } + + l.AnnotationTypes = orderedmap.New[string, *Shape](len(valueNode.Content) / 2) + // Map nodes come in pairs in order [key, value] + for j := 0; j != len(valueNode.Content); j += 2 { + name := valueNode.Content[j].Value + data := valueNode.Content[j+1] + shape, err := l.raml.makeShape(data, name, l.Location) + if err != nil { + return StacktraceNewWrapped("parse annotation types: make shape", err, l.Location, WithNodePosition(data)) + } + l.AnnotationTypes.Set(name, shape) + l.raml.PutAnnotationTypeIntoFragment(name, l.Location, shape) + l.raml.PutShapePtr(shape) + } + + return nil +} + // UnmarshalYAML unmarshals a Library from a yaml.Node, implementing the yaml.Unmarshaler interface func (l *Library) UnmarshalYAML(value *yaml.Node) error { if value.Kind != yaml.MappingNode { @@ -68,56 +129,12 @@ func (l *Library) UnmarshalYAML(value *yaml.Node) error { valueNode := value.Content[i+1] switch node.Value { case "uses": - if valueNode.Tag == TagNull { - continue - } - - l.Uses = orderedmap.New[string, *LibraryLink](len(valueNode.Content) / 2) - // Map nodes come in pairs in order [key, value] - for j := 0; j != len(valueNode.Content); j += 2 { - name := valueNode.Content[j].Value - path := valueNode.Content[j+1] - l.Uses.Set(name, &LibraryLink{ - Value: path.Value, - Location: l.Location, - Position: stacktrace.Position{Line: path.Line, Column: path.Column}, - }) - } + l.unmarshalUses(valueNode) case "types": - if valueNode.Tag == TagNull { - continue - } - - l.Types = orderedmap.New[string, *Shape](len(valueNode.Content) / 2) - // Map nodes come in pairs in order [key, value] - for j := 0; j != len(valueNode.Content); j += 2 { - name := valueNode.Content[j].Value - data := valueNode.Content[j+1] - shape, err := l.raml.makeShape(data, name, l.Location) - if err != nil { - return StacktraceNewWrapped("parse types: make shape", err, l.Location, WithNodePosition(data)) - } - l.Types.Set(name, shape) - l.raml.PutTypeIntoFragment(name, l.Location, shape) - l.raml.PutShapePtr(shape) - } + l.unmarshalTypes(valueNode) case "annotationTypes": - if valueNode.Tag == TagNull { - continue - } - - l.AnnotationTypes = orderedmap.New[string, *Shape](len(valueNode.Content) / 2) - // Map nodes come in pairs in order [key, value] - for j := 0; j != len(valueNode.Content); j += 2 { - name := valueNode.Content[j].Value - data := valueNode.Content[j+1] - shape, err := l.raml.makeShape(data, name, l.Location) - if err != nil { - return StacktraceNewWrapped("parse annotation types: make shape", err, l.Location, WithNodePosition(data)) - } - l.AnnotationTypes.Set(name, shape) - l.raml.PutAnnotationTypeIntoFragment(name, l.Location, shape) - l.raml.PutShapePtr(shape) + if err := l.unmarshalAnnotationTypes(valueNode); err != nil { + return fmt.Errorf("unmarshall annotation types: %w", err) } case "usage": if err := valueNode.Decode(&l.Usage); err != nil { diff --git a/node.go b/node.go index f99ac19..efa866f 100644 --- a/node.go +++ b/node.go @@ -39,56 +39,62 @@ func (n *Node) String() string { return fmt.Sprintf("%v", n.Value) } -func (r *RAML) makeRootNode(node *yaml.Node, location string) (*Node, error) { - if node.Tag == TagInclude { - baseDir := filepath.Dir(location) - fragmentPath := filepath.Join(baseDir, node.Value) - rdr, err := ReadRawFile(fragmentPath) +func (r *RAML) makeIncludedNode(node *yaml.Node, location string) (*Node, error) { + baseDir := filepath.Dir(location) + fragmentPath := filepath.Join(baseDir, node.Value) + rdr, err := ReadRawFile(fragmentPath) + if err != nil { + return nil, StacktraceNewWrapped("include: read raw file", err, location, WithNodePosition(node), + stacktrace.WithInfo("path", fragmentPath)) + } + defer func(rdr io.ReadCloser) { + err = rdr.Close() if err != nil { - return nil, StacktraceNewWrapped("include: read raw file", err, location, WithNodePosition(node), - stacktrace.WithInfo("path", fragmentPath)) + log.Fatal(fmt.Errorf("close file error: %w", err)) } - defer func(rdr io.ReadCloser) { - err = rdr.Close() - if err != nil { - log.Fatal(fmt.Errorf("close file error: %w", err)) - } - }(rdr) - var value any - ext := filepath.Ext(node.Value) - switch ext { - default: - v, errDecode := io.ReadAll(rdr) - if errDecode != nil { - return nil, StacktraceNewWrapped("include: read all", errDecode, fragmentPath, WithNodePosition(node)) - } - value = string(v) - case ".json": - var data any - d := json.NewDecoder(rdr) - if errDecode := d.Decode(&data); errDecode != nil { - return nil, StacktraceNewWrapped("include: json decode", errDecode, fragmentPath, WithNodePosition(node)) - } - value = data - case ".yaml", ".yml": - var data yaml.Node - d := yaml.NewDecoder(rdr) - if errDecode := d.Decode(&data); errDecode != nil { - return nil, StacktraceNewWrapped("include: yaml decode", errDecode, fragmentPath, WithNodePosition(node)) - } - value, err = yamlNodeToDataNode(&data, fragmentPath, false) - if err != nil { - return nil, StacktraceNewWrapped("include: yaml node to data node", err, fragmentPath, - WithNodePosition(node)) - } + }(rdr) + var value any + ext := filepath.Ext(node.Value) + switch ext { + default: + v, errDecode := io.ReadAll(rdr) + if errDecode != nil { + return nil, StacktraceNewWrapped("include: read all", errDecode, fragmentPath, WithNodePosition(node)) } - return &Node{ - Value: value, - Location: fragmentPath, - Position: stacktrace.Position{Line: node.Line, Column: node.Column}, - raml: r, - }, nil - } else if node.Value != "" && node.Value[0] == '{' { + value = string(v) + case ".json": + var data any + d := json.NewDecoder(rdr) + if errDecode := d.Decode(&data); errDecode != nil { + return nil, StacktraceNewWrapped("include: json decode", errDecode, fragmentPath, WithNodePosition(node)) + } + value = data + case ".yaml", ".yml": + var data yaml.Node + d := yaml.NewDecoder(rdr) + if errDecode := d.Decode(&data); errDecode != nil { + return nil, StacktraceNewWrapped("include: yaml decode", errDecode, fragmentPath, WithNodePosition(node)) + } + value, err = yamlNodeToDataNode(&data, fragmentPath, false) + if err != nil { + return nil, StacktraceNewWrapped("include: yaml node to data node", err, fragmentPath, + WithNodePosition(node)) + } + } + return &Node{ + Value: value, + Location: fragmentPath, + Position: stacktrace.Position{Line: node.Line, Column: node.Column}, + raml: r, + }, nil +} + +func (r *RAML) makeRootNode(node *yaml.Node, location string) (*Node, error) { + if node.Tag == TagInclude { + return r.makeIncludedNode(node, location) + } + + if node.Value != "" && node.Value[0] == '{' { var value any if err := json.Unmarshal([]byte(node.Value), &value); err != nil { return nil, StacktraceNewWrapped("json unmarshal", err, location, WithNodePosition(node)) @@ -117,6 +123,62 @@ func (r *RAML) makeYamlNode(node *yaml.Node, location string) (*Node, error) { }, nil } +func scalarNodeToDataNode(node *yaml.Node, location string, isInclude bool) (any, error) { + switch node.Tag { + default: + var val any + if err := node.Decode(&val); err != nil { + return nil, StacktraceNewWrapped("decode scalar node", err, location) + } + return val, nil + case TagStr: + return node.Value, nil + case TagTimestamp: + return node.Value, nil + case TagInclude: + if isInclude { + return nil, stacktrace.New("nested includes are not allowed", location, WithNodePosition(node)) + } + // TODO: In case with includes that are explicitly required to be string value, probably need to introduce + // a new tag. + // !includestr sounds like a good candidate. + baseDir := filepath.Dir(location) + fragmentPath := filepath.Join(baseDir, node.Value) + // TODO: Need to refactor and move out IO logic from this function. + r, err := ReadRawFile(filepath.Join(baseDir, node.Value)) + if err != nil { + return nil, StacktraceNewWrapped("include: read raw file", err, location, WithNodePosition(node), + stacktrace.WithInfo("path", fragmentPath)) + } + defer func(r io.ReadCloser) { + err = r.Close() + if err != nil { + log.Fatal(fmt.Errorf("close file error: %w", err)) + } + }(r) + // TODO: This logic should be more complex because content type may depend on the header reported + // by remote server. + ext := filepath.Ext(node.Value) + switch ext { + default: + v, errRead := io.ReadAll(r) + if errRead != nil { + return nil, StacktraceNewWrapped("include: read all", errRead, fragmentPath, + WithNodePosition(node)) + } + return string(v), nil + case ".yaml", ".yml": + var data yaml.Node + d := yaml.NewDecoder(r) + if errDecode := d.Decode(&data); errDecode != nil { + return nil, StacktraceNewWrapped("include: yaml decode", errDecode, fragmentPath, + WithNodePosition(node)) + } + return yamlNodeToDataNode(&data, fragmentPath, true) + } + } +} + func yamlNodeToDataNode(node *yaml.Node, location string, isInclude bool) (any, error) { switch node.Kind { default: @@ -127,59 +189,7 @@ func yamlNodeToDataNode(node *yaml.Node, location string, isInclude bool) (any, case yaml.DocumentNode: return yamlNodeToDataNode(node.Content[0], location, isInclude) case yaml.ScalarNode: - switch node.Tag { - default: - var val any - if err := node.Decode(&val); err != nil { - return nil, StacktraceNewWrapped("decode scalar node", err, location) - } - return val, nil - case TagStr: - return node.Value, nil - case TagTimestamp: - return node.Value, nil - case TagInclude: - if isInclude { - return nil, stacktrace.New("nested includes are not allowed", location, WithNodePosition(node)) - } - // TODO: In case with includes that are explicitly required to be string value, probably need to introduce - // a new tag. - // !includestr sounds like a good candidate. - baseDir := filepath.Dir(location) - fragmentPath := filepath.Join(baseDir, node.Value) - // TODO: Need to refactor and move out IO logic from this function. - r, err := ReadRawFile(filepath.Join(baseDir, node.Value)) - if err != nil { - return nil, StacktraceNewWrapped("include: read raw file", err, location, WithNodePosition(node), - stacktrace.WithInfo("path", fragmentPath)) - } - defer func(r io.ReadCloser) { - err = r.Close() - if err != nil { - log.Fatal(fmt.Errorf("close file error: %w", err)) - } - }(r) - // TODO: This logic should be more complex because content type may depend on the header reported - // by remote server. - ext := filepath.Ext(node.Value) - switch ext { - default: - v, errRead := io.ReadAll(r) - if errRead != nil { - return nil, StacktraceNewWrapped("include: read all", errRead, fragmentPath, - WithNodePosition(node)) - } - return string(v), nil - case ".yaml", ".yml": - var data yaml.Node - d := yaml.NewDecoder(r) - if errDecode := d.Decode(&data); errDecode != nil { - return nil, StacktraceNewWrapped("include: yaml decode", errDecode, fragmentPath, - WithNodePosition(node)) - } - return yamlNodeToDataNode(&data, fragmentPath, true) - } - } + return scalarNodeToDataNode(node, location, isInclude) case yaml.MappingNode: properties := make(map[string]any, len(node.Content)/2) for i := 0; i != len(node.Content); i += 2 { diff --git a/scalars.go b/scalars.go index a46000b..89da143 100644 --- a/scalars.go +++ b/scalars.go @@ -80,11 +80,11 @@ func (s *IntegerShape) Clone() Shape { return &c } -func (s *IntegerShape) clone(history []Shape) Shape { +func (s *IntegerShape) clone(_ []Shape) Shape { return s.Clone() } -func (s *IntegerShape) Validate(v interface{}, ctxPath string) error { +func (s *IntegerShape) Validate(v interface{}, _ string) error { var val big.Int switch v := v.(type) { case int: @@ -271,11 +271,11 @@ func (s *NumberShape) Clone() Shape { return &c } -func (s *NumberShape) clone(history []Shape) Shape { +func (s *NumberShape) clone(_ []Shape) Shape { return s.Clone() } -func (s *NumberShape) Validate(v interface{}, ctxPath string) error { +func (s *NumberShape) Validate(v interface{}, _ string) error { var val float64 switch v := v.(type) { // go-yaml unmarshals integers as int @@ -447,11 +447,11 @@ func (s *StringShape) Clone() Shape { return &c } -func (s *StringShape) clone(history []Shape) Shape { +func (s *StringShape) clone(_ []Shape) Shape { return s.Clone() } -func (s *StringShape) Validate(v interface{}, ctxPath string) error { +func (s *StringShape) Validate(v interface{}, _ string) error { i, ok := v.(string) if !ok { return fmt.Errorf("invalid type, got %T, expected string", v) @@ -599,11 +599,11 @@ func (s *FileShape) Clone() Shape { return &c } -func (s *FileShape) clone(history []Shape) Shape { +func (s *FileShape) clone(_ []Shape) Shape { return s.Clone() } -func (s *FileShape) Validate(v interface{}, ctxPath string) error { +func (s *FileShape) Validate(v interface{}, _ string) error { i, ok := v.(string) if !ok { return fmt.Errorf("invalid type, got %T, expected string", v) @@ -729,11 +729,11 @@ func (s *BooleanShape) Clone() Shape { return &c } -func (s *BooleanShape) clone(history []Shape) Shape { +func (s *BooleanShape) clone(_ []Shape) Shape { return s.Clone() } -func (s *BooleanShape) Validate(v interface{}, ctxPath string) error { +func (s *BooleanShape) Validate(v interface{}, _ string) error { i, ok := v.(bool) if !ok { return fmt.Errorf("invalid type, got %T, expected bool", v) @@ -819,11 +819,11 @@ func (s *DateTimeShape) Clone() Shape { return &c } -func (s *DateTimeShape) clone(history []Shape) Shape { +func (s *DateTimeShape) clone(_ []Shape) Shape { return s.Clone() } -func (s *DateTimeShape) Validate(v interface{}, ctxPath string) error { +func (s *DateTimeShape) Validate(v interface{}, _ string) error { i, ok := v.(string) if !ok { return fmt.Errorf("invalid type, got %T, expected string", v) @@ -907,11 +907,11 @@ func (s *DateTimeOnlyShape) Clone() Shape { return &c } -func (s *DateTimeOnlyShape) clone(history []Shape) Shape { +func (s *DateTimeOnlyShape) clone(_ []Shape) Shape { return s.Clone() } -func (s *DateTimeOnlyShape) Validate(v interface{}, ctxPath string) error { +func (s *DateTimeOnlyShape) Validate(v interface{}, _ string) error { i, ok := v.(string) if !ok { return fmt.Errorf("invalid type, got %T, expected string", v) @@ -964,11 +964,11 @@ func (s *DateOnlyShape) Clone() Shape { return &c } -func (s *DateOnlyShape) clone(history []Shape) Shape { +func (s *DateOnlyShape) clone(_ []Shape) Shape { return s.Clone() } -func (s *DateOnlyShape) Validate(v interface{}, ctxPath string) error { +func (s *DateOnlyShape) Validate(v interface{}, _ string) error { i, ok := v.(string) if !ok { return fmt.Errorf("invalid type, got %T, expected string", v) @@ -1021,11 +1021,11 @@ func (s *TimeOnlyShape) Clone() Shape { return &c } -func (s *TimeOnlyShape) clone(history []Shape) Shape { +func (s *TimeOnlyShape) clone(_ []Shape) Shape { return s.Clone() } -func (s *TimeOnlyShape) Validate(v interface{}, ctxPath string) error { +func (s *TimeOnlyShape) Validate(v interface{}, _ string) error { i, ok := v.(string) if !ok { return fmt.Errorf("invalid type, got %T, expected string", v) @@ -1078,12 +1078,12 @@ func (s *AnyShape) Clone() Shape { return &c } -func (s *AnyShape) clone(history []Shape) Shape { +func (s *AnyShape) clone(_ []Shape) Shape { return s.Clone() } // Validate checks if the value is nil, implements Shape interface -func (s *AnyShape) Validate(v interface{}, ctxPath string) error { +func (s *AnyShape) Validate(_ interface{}, _ string) error { return nil } @@ -1128,12 +1128,12 @@ func (s *NilShape) Clone() Shape { } // clone returns a copy of the shape -func (s *NilShape) clone(history []Shape) Shape { +func (s *NilShape) clone(_ []Shape) Shape { return s.Clone() } // Validate checks if the value is nil, implements Shape interface -func (s *NilShape) Validate(v interface{}, ctxPath string) error { +func (s *NilShape) Validate(v interface{}, _ string) error { if v != nil { return fmt.Errorf("invalid type, got %T, expected nil", v) } diff --git a/shape.go b/shape.go index 2ccf98b..ceb1e3e 100644 --- a/shape.go +++ b/shape.go @@ -257,6 +257,77 @@ func generateShapeID() string { return id } +func (r *RAML) makeShapeType( + shapeTypeNode *yaml.Node, shapeFacets []*yaml.Node, + name, location string, base *BaseShape) (string, *Shape, error) { + var shape *Shape + var shapeType string + + switch shapeTypeNode.Kind { + default: + return shapeType, shape, stacktrace.New("type must be string or array", location, + WithNodePosition(shapeTypeNode)) + case yaml.DocumentNode: + return shapeType, shape, stacktrace.New("document node is not allowed", location, + WithNodePosition(shapeTypeNode)) + case yaml.MappingNode: + return shapeType, shape, stacktrace.New("mapping node is not allowed", location, + WithNodePosition(shapeTypeNode)) + case yaml.AliasNode: + return shapeType, shape, stacktrace.New("alias node is not allowed", location, + WithNodePosition(shapeTypeNode)) + case yaml.ScalarNode: + switch shapeTypeNode.Tag { + case TagStr: + shapeType = shapeTypeNode.Value + if shapeType == "" { + shapeType = identifyShapeType(shapeFacets) + } else if shapeType[0] == '{' { + s, errMake := r.MakeJSONShape(base, shapeType) + if errMake != nil { + return "", nil, StacktraceNewWrapped("make json shape", errMake, location, + WithNodePosition(shapeTypeNode)) + } + return shapeType, &s, nil + } + case TagInclude: + baseDir := filepath.Dir(location) + dt, errParse := r.parseDataType(filepath.Join(baseDir, shapeTypeNode.Value)) + if errParse != nil { + return shapeType, shape, StacktraceNewWrapped("parse data", errParse, location, + WithNodePosition(shapeTypeNode)) + } + base.TypeLabel = shapeTypeNode.Value + base.Link = dt + case TagNull: + shapeType = TypeString + default: + return shapeType, shape, stacktrace.New("type must be string", location, + WithNodePosition(shapeTypeNode)) + } + case yaml.SequenceNode: + var inherits = make([]*Shape, len(shapeTypeNode.Content)) + for i, node := range shapeTypeNode.Content { + if node.Kind != yaml.ScalarNode { + return shapeType, shape, stacktrace.New("node kind must be scalar", location, + WithNodePosition(node)) + } else if node.Tag == "!include" { + return shapeType, shape, stacktrace.New("!include is not allowed in multiple inheritance", + location, WithNodePosition(node)) + } + s, errMake := r.makeShape(node, name, location) + if errMake != nil { + return shapeType, shape, StacktraceNewWrapped("make shape", errMake, location, + WithNodePosition(node)) + } + inherits[i] = s + } + base.Inherits = inherits + shapeType = TypeComposite + } + return shapeType, shape, nil +} + // makeShape creates a new shape from the given YAML node. func (r *RAML) makeShape(v *yaml.Node, name string, location string) (*Shape, error) { base := r.MakeBaseShape(name, location, &stacktrace.Position{Line: v.Line, Column: v.Column}) @@ -270,64 +341,13 @@ func (r *RAML) makeShape(v *yaml.Node, name string, location string) (*Shape, er if shapeTypeNode == nil { shapeType = identifyShapeType(shapeFacets) } else { - switch shapeTypeNode.Kind { - default: - return nil, stacktrace.New("type must be string or array", location, - WithNodePosition(shapeTypeNode)) - case yaml.DocumentNode: - return nil, stacktrace.New("document node is not allowed", location, - WithNodePosition(shapeTypeNode)) - case yaml.MappingNode: - return nil, stacktrace.New("mapping node is not allowed", location, - WithNodePosition(shapeTypeNode)) - case yaml.AliasNode: - return nil, stacktrace.New("alias node is not allowed", location, - WithNodePosition(shapeTypeNode)) - case yaml.ScalarNode: - switch shapeTypeNode.Tag { - case TagStr: - shapeType = shapeTypeNode.Value - if shapeType == "" { - shapeType = identifyShapeType(shapeFacets) - } else if shapeType[0] == '{' { - s, errMake := r.MakeJSONShape(base, shapeType) - if errMake != nil { - return nil, StacktraceNewWrapped("make json shape", errMake, location, - WithNodePosition(shapeTypeNode)) - } - return &s, nil - } - case TagInclude: - baseDir := filepath.Dir(location) - dt, errParse := r.parseDataType(filepath.Join(baseDir, shapeTypeNode.Value)) - if errParse != nil { - return nil, StacktraceNewWrapped("parse data", errParse, location, - WithNodePosition(shapeTypeNode)) - } - base.TypeLabel = shapeTypeNode.Value - base.Link = dt - case TagNull: - shapeType = TypeString - default: - return nil, stacktrace.New("type must be string", location, WithNodePosition(shapeTypeNode)) - } - case yaml.SequenceNode: - var inherits = make([]*Shape, len(shapeTypeNode.Content)) - for i, node := range shapeTypeNode.Content { - if node.Kind != yaml.ScalarNode { - return nil, stacktrace.New("node kind must be scalar", location, WithNodePosition(node)) - } else if node.Tag == "!include" { - return nil, stacktrace.New("!include is not allowed in multiple inheritance", - location, WithNodePosition(node)) - } - s, errMake := r.makeShape(node, name, location) - if errMake != nil { - return nil, StacktraceNewWrapped("make shape", errMake, location, WithNodePosition(node)) - } - inherits[i] = s - } - base.Inherits = inherits - shapeType = TypeComposite + var shape *Shape + shapeType, shape, err = r.makeShapeType(shapeTypeNode, shapeFacets, name, location, base) + if err != nil { + return nil, fmt.Errorf("make shape type: %w", err) + } + if shape != nil { + return shape, nil } } @@ -343,6 +363,131 @@ func (r *RAML) makeShape(v *yaml.Node, name string, location string) (*Shape, er return ptr, nil } +func (s *BaseShape) decodeExamples(valueNode *yaml.Node) error { + if s.Example != nil { + return stacktrace.New("example and examples cannot be defined together", s.Location, + WithNodePosition(valueNode)) + } + if valueNode.Kind == yaml.ScalarNode && valueNode.Tag == "!include" { + baseDir := filepath.Dir(s.Location) + n, err := s.raml.parseNamedExample(filepath.Join(baseDir, valueNode.Value)) + if err != nil { + return StacktraceNewWrapped("parse named example", err, s.Location, + WithNodePosition(valueNode)) + } + s.Examples = &Examples{Link: n, Location: s.Location} + return nil + } else if valueNode.Kind != yaml.MappingNode { + return stacktrace.New("examples must be map", s.Location, + WithNodePosition(valueNode)) + } + examples := orderedmap.New[string, *Example](len(valueNode.Content) / 2) + for j := 0; j != len(valueNode.Content); j += 2 { + name := valueNode.Content[j].Value + data := valueNode.Content[j+1] + example, err := s.raml.makeExample(data, name, s.Location) + if err != nil { + return StacktraceNewWrapped(fmt.Sprintf("make examples: [%d]", j), + err, s.Location, WithNodePosition(data)) + } + examples.Set(name, example) + } + s.Examples = &Examples{Map: examples, Location: s.Location} + return nil +} + +func (s *BaseShape) decodeFacets(valueNode *yaml.Node) error { + s.CustomShapeFacetDefinitions = orderedmap.New[string, Property](len(valueNode.Content) / 2) + for j := 0; j != len(valueNode.Content); j += 2 { + nodeName := valueNode.Content[j].Value + data := valueNode.Content[j+1] + + propertyName, hasImplicitOptional := s.raml.chompImplicitOptional(nodeName) + property, err := s.raml.makeProperty(nodeName, propertyName, data, s.Location, hasImplicitOptional) + if err != nil { + return StacktraceNewWrapped("make property", err, s.Location, + WithNodePosition(data)) + } + s.CustomShapeFacetDefinitions.Set(property.Name, property) + } + return nil +} + +func (s *BaseShape) decodeExample(valueNode *yaml.Node) error { + if s.Examples != nil { + return stacktrace.New("example and examples cannot be defined together", s.Location, + WithNodePosition(valueNode)) + } + example, err := s.raml.makeExample(valueNode, "", s.Location) + if err != nil { + return StacktraceNewWrapped("make example", err, s.Location, + WithNodePosition(valueNode)) + } + s.Example = example + return nil +} + +func (s *BaseShape) decodeValueNode(node, valueNode *yaml.Node) (*yaml.Node, []*yaml.Node, error) { + var shapeTypeNode *yaml.Node + shapeFacets := make([]*yaml.Node, 0) + + switch node.Value { + case "type": + shapeTypeNode = valueNode + case "displayName": + if err := valueNode.Decode(&s.DisplayName); err != nil { + return nil, nil, StacktraceNewWrapped("decode display name", err, s.Location, + WithNodePosition(valueNode)) + } + case "description": + if err := valueNode.Decode(&s.Description); err != nil { + return nil, nil, StacktraceNewWrapped("decode description", err, s.Location, + WithNodePosition(valueNode)) + } + case "required": + if err := valueNode.Decode(&s.Required); err != nil { + return nil, nil, StacktraceNewWrapped("decode required", err, s.Location, + WithNodePosition(valueNode)) + } + case "facets": + if err := s.decodeFacets(valueNode); err != nil { + return nil, nil, StacktraceNewWrapped("decode facets", err, s.Location, + WithNodePosition(valueNode)) + } + case "example": + if err := s.decodeExample(valueNode); err != nil { + return nil, nil, StacktraceNewWrapped("decode example", err, s.Location, + WithNodePosition(valueNode)) + } + case "examples": + if err := s.decodeExamples(valueNode); err != nil { + return nil, nil, StacktraceNewWrapped("decode example", err, s.Location, + WithNodePosition(valueNode)) + } + case "default": + n, err := s.raml.makeRootNode(valueNode, s.Location) + if err != nil { + return nil, nil, StacktraceNewWrapped("make node default", err, s.Location, + WithNodePosition(valueNode)) + } + s.Default = n + case "allowedTargets": + // TODO: Included by annotationTypes + default: + if IsCustomDomainExtensionNode(node.Value) { + name, de, err := s.raml.unmarshalCustomDomainExtension(s.Location, node, valueNode) + if err != nil { + return nil, nil, StacktraceNewWrapped("unmarshal custom domain extension", err, s.Location, + WithNodePosition(valueNode)) + } + s.CustomDomainProperties.Set(name, de) + } else { + shapeFacets = append(shapeFacets, node, valueNode) + } + } + return shapeTypeNode, shapeFacets, nil +} + // decode decodes the shape from the YAML node. func (s *BaseShape) decode(value *yaml.Node) (*yaml.Node, []*yaml.Node, error) { // For inline type declaration @@ -360,99 +505,15 @@ func (s *BaseShape) decode(value *yaml.Node) (*yaml.Node, []*yaml.Node, error) { for i := 0; i != len(value.Content); i += 2 { node := value.Content[i] valueNode := value.Content[i+1] - switch node.Value { - case "type": - shapeTypeNode = valueNode - case "displayName": - if err := valueNode.Decode(&s.DisplayName); err != nil { - return nil, nil, StacktraceNewWrapped("decode display name", err, s.Location, - WithNodePosition(valueNode)) - } - case "description": - if err := valueNode.Decode(&s.Description); err != nil { - return nil, nil, StacktraceNewWrapped("decode description", err, s.Location, - WithNodePosition(valueNode)) - } - case "required": - if err := valueNode.Decode(&s.Required); err != nil { - return nil, nil, StacktraceNewWrapped("decode required", err, s.Location, - WithNodePosition(valueNode)) - } - case "facets": - s.CustomShapeFacetDefinitions = orderedmap.New[string, Property](len(valueNode.Content) / 2) - for j := 0; j != len(valueNode.Content); j += 2 { - nodeName := valueNode.Content[j].Value - data := valueNode.Content[j+1] - - propertyName, hasImplicitOptional := s.raml.chompImplicitOptional(nodeName) - property, err := s.raml.makeProperty(nodeName, propertyName, data, s.Location, hasImplicitOptional) - if err != nil { - return nil, nil, StacktraceNewWrapped("make property", err, s.Location, - WithNodePosition(data)) - } - s.CustomShapeFacetDefinitions.Set(property.Name, property) - } - case "example": - if s.Examples != nil { - return nil, nil, stacktrace.New("example and examples cannot be defined together", s.Location, - WithNodePosition(valueNode)) - } - example, err := s.raml.makeExample(valueNode, "", s.Location) - if err != nil { - return nil, nil, StacktraceNewWrapped("make example", err, s.Location, - WithNodePosition(valueNode)) - } - s.Example = example - case "examples": - if s.Example != nil { - return nil, nil, stacktrace.New("example and examples cannot be defined together", s.Location, - WithNodePosition(valueNode)) - } - if valueNode.Kind == yaml.ScalarNode && valueNode.Tag == "!include" { - baseDir := filepath.Dir(s.Location) - n, err := s.raml.parseNamedExample(filepath.Join(baseDir, valueNode.Value)) - if err != nil { - return nil, nil, StacktraceNewWrapped("parse named example", err, s.Location, - WithNodePosition(valueNode)) - } - s.Examples = &Examples{Link: n, Location: s.Location} - continue - } else if valueNode.Kind != yaml.MappingNode { - return nil, nil, stacktrace.New("examples must be map", s.Location, - WithNodePosition(valueNode)) - } - examples := orderedmap.New[string, *Example](len(valueNode.Content) / 2) - for j := 0; j != len(valueNode.Content); j += 2 { - name := valueNode.Content[j].Value - data := valueNode.Content[j+1] - example, err := s.raml.makeExample(data, name, s.Location) - if err != nil { - return nil, nil, StacktraceNewWrapped(fmt.Sprintf("make examples: [%d]", j), - err, s.Location, WithNodePosition(data)) - } - examples.Set(name, example) - } - s.Examples = &Examples{Map: examples, Location: s.Location} - case "default": - n, err := s.raml.makeRootNode(valueNode, s.Location) - if err != nil { - return nil, nil, StacktraceNewWrapped("make node default", err, s.Location, - WithNodePosition(valueNode)) - } - s.Default = n - case "allowedTargets": - // TODO: Included by annotationTypes - default: - if IsCustomDomainExtensionNode(node.Value) { - name, de, err := s.raml.unmarshalCustomDomainExtension(s.Location, node, valueNode) - if err != nil { - return nil, nil, StacktraceNewWrapped("unmarshal custom domain extension", err, s.Location, - WithNodePosition(valueNode)) - } - s.CustomDomainProperties.Set(name, de) - } else { - shapeFacets = append(shapeFacets, node, valueNode) - } + t, f, err := s.decodeValueNode(node, valueNode) + if err != nil { + return nil, nil, fmt.Errorf("decode value node: %w", err) + } + if t != nil { + shapeTypeNode = t + } + if len(f) > 0 { + shapeFacets = append(shapeFacets, f...) } } diff --git a/unwrap.go b/unwrap.go index bb911df..f4f35b3 100644 --- a/unwrap.go +++ b/unwrap.go @@ -7,101 +7,100 @@ import ( orderedmap "github.com/wk8/go-ordered-map/v2" ) -/* -UnwrapShapes unwraps all shapes in the RAML. +func (r *RAML) unwrapTypes(types *orderedmap.OrderedMap[string, *Shape], f *Library) *stacktrace.StackTrace { + var st *stacktrace.StackTrace + for pair := types.Oldest(); pair != nil; pair = pair.Next() { + k, shape := pair.Key, pair.Value + if shape == nil { + se := stacktrace.New("shape is nil", f.Location, + stacktrace.WithType(stacktrace.TypeUnwrapping)) + if st == nil { + st = se + } else { + st = st.Append(se) + } + continue + } + position := (*shape).Base().Position + us, err := r.UnwrapShape(shape, make([]Shape, 0)) + if err != nil { + se := StacktraceNewWrapped("unwrap shape", err, f.Location, + stacktrace.WithType(stacktrace.TypeUnwrapping), stacktrace.WithPosition(&position)) + if st == nil { + st = se + } else { + st = st.Append(se) + } + continue + } + ptr := &us + types.Set(k, ptr) + r.PutAnnotationTypeIntoFragment(us.Base().Name, f.Location, ptr) + r.PutShapePtr(ptr) + } + return st +} -NOTE: With unwrap, we replace pointers to definitions instead of values by pointers to keep original parents unchanged. -Otherwise, parents will be also modified and unwrap may produce unpredictable results. +func (r *RAML) unwrapLibrary(f *Library) *stacktrace.StackTrace { + st := r.unwrapTypes(f.AnnotationTypes, f) + se := r.unwrapTypes(f.Types, f) + if se != nil { + if st == nil { + st = se + } else { + st = st.Append(se) + } + } + return st +} -Unfortunately, this is required to properly support recursive shapes. -A more sophisticated approach is required to save memory and avoid copies. -*/ -func (r *RAML) UnwrapShapes() error { - // We need to invalidate old cache and re-populate it because references will no longer be valid after unwrapping. - r.fragmentTypes = make(map[string]map[string]*Shape) - r.fragmentAnnotationTypes = make(map[string]map[string]*Shape) - r.shapes = nil +func (r *RAML) unwrapDataType(f *DataType) *stacktrace.StackTrace { + if f.Shape == nil { + return stacktrace.New("shape is nil", f.Location, + stacktrace.WithType(stacktrace.TypeUnwrapping)) + } + position := (*f.Shape).Base().Position + us, err := r.UnwrapShape(f.Shape, make([]Shape, 0)) + if err != nil { + return StacktraceNewWrapped("unwrap shape", err, f.Location, + stacktrace.WithType(stacktrace.TypeUnwrapping), stacktrace.WithPosition(&position)) + } + ptr := &us + f.Shape = ptr + r.PutTypeIntoFragment(us.Base().Name, f.Location, ptr) + r.PutShapePtr(ptr) + return nil +} + +func (r *RAML) unwrapFragments() *stacktrace.StackTrace { var st *stacktrace.StackTrace - st = nil for _, frag := range r.fragmentsCache { switch f := frag.(type) { case *Library: - for pair := f.AnnotationTypes.Oldest(); pair != nil; pair = pair.Next() { - k, shape := pair.Key, pair.Value - if shape == nil { - se := stacktrace.New("shape is nil", f.Location, - stacktrace.WithType(stacktrace.TypeUnwrapping)) - if st == nil { - st = se - } else { - st = st.Append(se) - } - continue - } - position := (*shape).Base().Position - us, err := r.UnwrapShape(shape, make([]Shape, 0)) - if err != nil { - se := StacktraceNewWrapped("unwrap shape", err, f.Location, - stacktrace.WithType(stacktrace.TypeUnwrapping), stacktrace.WithPosition(&position)) - if st == nil { - st = se - } else { - st = st.Append(se) - } - continue - } - ptr := &us - f.AnnotationTypes.Set(k, ptr) - r.PutAnnotationTypeIntoFragment(us.Base().Name, f.Location, ptr) - r.PutShapePtr(ptr) - } - for pair := f.Types.Oldest(); pair != nil; pair = pair.Next() { - k, shape := pair.Key, pair.Value - if shape == nil { - se := stacktrace.New("shape is nil", f.Location, - stacktrace.WithType(stacktrace.TypeUnwrapping)) - if st == nil { - st = se - } else { - st = st.Append(se) - } - continue - } - position := (*shape).Base().Position - us, err := r.UnwrapShape(shape, make([]Shape, 0)) - if err != nil { - se := StacktraceNewWrapped("unwrap shape", err, f.Location, - stacktrace.WithType(stacktrace.TypeUnwrapping), stacktrace.WithPosition(&position)) - if st == nil { - st = se - } else { - st = st.Append(se) - } - continue + se := r.unwrapLibrary(f) + if se != nil { + if st == nil { + st = se + } else { + st = st.Append(se) } - ptr := &us - f.Types.Set(k, ptr) - r.PutTypeIntoFragment(us.Base().Name, f.Location, ptr) - r.PutShapePtr(ptr) } case *DataType: - if f.Shape == nil { - return stacktrace.New("shape is nil", f.Location, - stacktrace.WithType(stacktrace.TypeUnwrapping)) - } - position := (*f.Shape).Base().Position - us, err := r.UnwrapShape(f.Shape, make([]Shape, 0)) - if err != nil { - return StacktraceNewWrapped("unwrap shape", err, f.Location, - stacktrace.WithType(stacktrace.TypeUnwrapping), stacktrace.WithPosition(&position)) + se := r.unwrapDataType(f) + if se != nil { + if st == nil { + st = se + } else { + st = st.Append(se) + } } - ptr := &us - f.Shape = ptr - r.PutTypeIntoFragment(us.Base().Name, f.Location, ptr) - r.PutShapePtr(ptr) } } - // Links to definedBy must be updated after unwrapping. + return st +} + +func (r *RAML) unwrapDomainExtensions() *stacktrace.StackTrace { + var st *stacktrace.StackTrace for _, item := range r.domainExtensions { db := *item.DefinedBy ptr, err := r.GetAnnotationTypeFromFragmentPtr(db.Base().Location, db.Base().Name) @@ -117,6 +116,33 @@ func (r *RAML) UnwrapShapes() error { } item.DefinedBy = ptr } + return st +} + +/* +UnwrapShapes unwraps all shapes in the RAML. + +NOTE: With unwrap, we replace pointers to definitions instead of values by pointers to keep original parents unchanged. +Otherwise, parents will be also modified and unwrap may produce unpredictable results. + +Unfortunately, this is required to properly support recursive shapes. +A more sophisticated approach is required to save memory and avoid copies. +*/ +func (r *RAML) UnwrapShapes() error { + // We need to invalidate old cache and re-populate it because references will no longer be valid after unwrapping. + r.fragmentTypes = make(map[string]map[string]*Shape) + r.fragmentAnnotationTypes = make(map[string]map[string]*Shape) + r.shapes = nil + st := r.unwrapFragments() + // Links to definedBy must be updated after unwrapping. + se := r.unwrapDomainExtensions() + if se != nil { + if st == nil { + st = se + } else { + st = st.Append(se) + } + } if st != nil { return st } @@ -160,9 +186,82 @@ func (r *RAML) inheritBase(sourceBase *BaseShape, targetBase *BaseShape) { // But maybe they can be inheritable in other context? } +func (r *RAML) inheritUnionsToSrc(target Shape, sourceUnion *UnionShape) (Shape, error) { + var filtered []*Shape + var st *stacktrace.StackTrace + for _, item := range sourceUnion.AnyOf { + i := *item + // If at least one union member has any type, the whole union is considered as any type. + if _, ok := i.(*AnyShape); ok { + return target, nil + } + if i.Base().Type == target.Base().Type { + // Deep copy with ID change is required since we create new union members from source members + cs := target.Clone() + // TODO: Probably all copied shapes must change IDs since these are actually new shapes. + cs.Base().ID = generateShapeID() + ms, err := cs.Inherit(i) + if err != nil { + se := StacktraceNewWrapped("merge shapes", err, target.Base().Location, + stacktrace.WithPosition(&target.Base().Position)) + if st == nil { + st = se + } else { + st = st.Append(se) + } + // Skip shapes that didn't pass inheritance check + continue + } + filtered = append(filtered, &ms) + } + } + if len(filtered) == 0 { + se := stacktrace.New("failed to find compatible union member", target.Base().Location, + stacktrace.WithPosition(&target.Base().Position)) + if st != nil { + se = se.Append(st) + } + return nil, se + } + // If only one union member remains - simplify to target type + if len(filtered) == 1 { + return *filtered[0], nil + } + // Convert target to union + target.Base().Type = TypeUnion + return &UnionShape{ + BaseShape: *target.Base(), + UnionFacets: UnionFacets{ + AnyOf: filtered, + }, + }, nil +} + +func (r *RAML) inheritUnionsToTarget( + target, source Shape, targetUnion *UnionShape) (Shape, error) { + var st *stacktrace.StackTrace + for _, item := range targetUnion.AnyOf { + // Merge will raise an error in case any of union members has incompatible type + _, err := (*item).Inherit(source) + if err != nil { + se := StacktraceNewWrapped("merge shapes", err, target.Base().Location, + stacktrace.WithPosition(&target.Base().Position)) + if st == nil { + st = se + } else { + st = st.Append(se) + } + continue + } + } + if st != nil { + return nil, st + } + return targetUnion, nil +} + // Inherit merges source shape into target shape. func (r *RAML) Inherit(source Shape, target Shape) (Shape, error) { - var st *stacktrace.StackTrace r.inheritBase(source.Base(), target.Base()) // If source is recursive, return source as is if _, ok := source.(*RecursiveShape); ok { @@ -178,72 +277,10 @@ func (r *RAML) Inherit(source Shape, target Shape) (Shape, error) { switch { case isSourceUnion && !isTargetUnion: - var filtered []*Shape - for _, item := range sourceUnion.AnyOf { - i := *item - // If at least one union member has any type, the whole union is considered as any type. - if _, ok := i.(*AnyShape); ok { - return target, nil - } - if i.Base().Type == target.Base().Type { - // Deep copy with ID change is required since we create new union members from source members - cs := target.Clone() - // TODO: Probably all copied shapes must change IDs since these are actually new shapes. - cs.Base().ID = generateShapeID() - ms, err := cs.Inherit(i) - if err != nil { - se := StacktraceNewWrapped("merge shapes", err, target.Base().Location, - stacktrace.WithPosition(&target.Base().Position)) - if st == nil { - st = se - } else { - st = st.Append(se) - } - // Skip shapes that didn't pass inheritance check - continue - } - filtered = append(filtered, &ms) - } - } - if len(filtered) == 0 { - se := stacktrace.New("failed to find compatible union member", target.Base().Location, - stacktrace.WithPosition(&target.Base().Position)) - if st != nil { - se = se.Append(st) - } - return nil, se - } - // If only one union member remains - simplify to target type - if len(filtered) == 1 { - return *filtered[0], nil - } - // Convert target to union - target.Base().Type = TypeUnion - return &UnionShape{ - BaseShape: *target.Base(), - UnionFacets: UnionFacets{ - AnyOf: filtered, - }, - }, nil + return r.inheritUnionsToSrc(target, sourceUnion) + case isTargetUnion && !isSourceUnion: - for _, item := range targetUnion.AnyOf { - // Merge will raise an error in case any of union members has incompatible type - _, err := (*item).Inherit(source) - if err != nil { - se := StacktraceNewWrapped("merge shapes", err, target.Base().Location, - stacktrace.WithPosition(&target.Base().Position)) - if st == nil { - st = se - } else { - st = st.Append(se) - } - continue - } - } - if st != nil { - return nil, st - } - return targetUnion, nil + return r.inheritUnionsToTarget(target, source, targetUnion) } // Homogenous types produce same type ms, err := target.Inherit(source) @@ -254,29 +291,53 @@ func (r *RAML) Inherit(source Shape, target Shape) (Shape, error) { return ms, nil } -// Recursively copies and unwraps a shape. -// Note that this method removes information about links. -func (r *RAML) UnwrapShape(s *Shape, history []Shape) (Shape, error) { - if s == nil { - return nil, fmt.Errorf("shape is nil") +func (r *RAML) unwrapObjShape(base *BaseShape, objShape *ObjectShape, history []Shape) error { + if objShape.Properties != nil { + for pair := objShape.Properties.Oldest(); pair != nil; pair = pair.Next() { + prop := pair.Value + us, err := r.UnwrapShape(prop.Shape, history) + if err != nil { + return StacktraceNewWrapped("object property unwrap", err, base.Location, + stacktrace.WithPosition(&base.Position), stacktrace.WithType(stacktrace.TypeUnwrapping)) + } + *prop.Shape = us + } } - // Perform deep copy to avoid modifying the original shape - target := (*s).Clone() - - base := target.Base() - // Skip already unwrapped shapes - if base.IsUnwrapped() { - return target, nil + if objShape.PatternProperties != nil { + for pair := objShape.PatternProperties.Oldest(); pair != nil; pair = pair.Next() { + prop := pair.Value + us, err := r.UnwrapShape(prop.Shape, history) + if err != nil { + return StacktraceNewWrapped("object pattern property unwrap", err, base.Location, + stacktrace.WithPosition(&base.Position), stacktrace.WithType(stacktrace.TypeUnwrapping)) + } + *prop.Shape = us + } } - for _, item := range history { - if item.Base().ID == base.ID { - base.Inherits = nil - return &RecursiveShape{BaseShape: *base, Head: &item}, nil + return nil +} +func (r *RAML) unwrapUnionShape(base *BaseShape, unionShape *UnionShape, history []Shape) error { + for _, item := range unionShape.AnyOf { + us, err := r.UnwrapShape(item, history) + if err != nil { + return StacktraceNewWrapped("union unwrap", err, base.Location, + stacktrace.WithPosition(&base.Position), stacktrace.WithType(stacktrace.TypeUnwrapping)) } + *item = us } - history = append(history, target) + return nil +} +func (r *RAML) unwrapSourceIfObj(src *Shape, history []Shape) (Shape, error) { + if src == nil { + return nil, fmt.Errorf("source is nil") + } + srcShape := *src + base, isObjShape := srcShape.(*ObjectShape) + if !isObjShape { + return srcShape, nil + } var source Shape switch { case base.Alias != nil: @@ -322,62 +383,80 @@ func (r *RAML) UnwrapShape(s *Shape, history []Shape) (Shape, error) { source = ss base.Inherits = unwrappedInherits } + return source, nil +} - if arrayShape, isArray := target.(*ArrayShape); isArray && arrayShape.Items != nil { - us, err := r.UnwrapShape(arrayShape.Items, history) - if err != nil { - return nil, StacktraceNewWrapped("array item unwrap", err, base.Location, - stacktrace.WithPosition(&base.Position), stacktrace.WithType(stacktrace.TypeUnwrapping)) - } - *arrayShape.Items = us - } else if objShape, ok := target.(*ObjectShape); ok { - if objShape.Properties != nil { - for pair := objShape.Properties.Oldest(); pair != nil; pair = pair.Next() { - prop := pair.Value - us, err := r.UnwrapShape(prop.Shape, history) - if err != nil { - return nil, StacktraceNewWrapped("object property unwrap", err, base.Location, - stacktrace.WithPosition(&base.Position), stacktrace.WithType(stacktrace.TypeUnwrapping)) - } - *prop.Shape = us +func (r *RAML) unwrapTarget(target Shape, history []Shape) error { + switch trg := target.(type) { + case *ArrayShape: + if trg.Items != nil { + us, err := r.UnwrapShape(trg.Items, history) + if err != nil { + return StacktraceNewWrapped("array item unwrap", err, target.Base().Location, + stacktrace.WithPosition(&target.Base().Position), stacktrace.WithType(stacktrace.TypeUnwrapping)) } + *trg.Items = us } - if objShape.PatternProperties != nil { - for pair := objShape.PatternProperties.Oldest(); pair != nil; pair = pair.Next() { - prop := pair.Value - us, err := r.UnwrapShape(prop.Shape, history) - if err != nil { - return nil, StacktraceNewWrapped("object pattern property unwrap", err, base.Location, - stacktrace.WithPosition(&base.Position), stacktrace.WithType(stacktrace.TypeUnwrapping)) - } - *prop.Shape = us - } + case *ObjectShape: + if err := r.unwrapObjShape(target.Base(), trg, history); err != nil { + return fmt.Errorf("unwrap object shape: %w", err) } - } else if unionShape, isUnion := target.(*UnionShape); isUnion { - for _, item := range unionShape.AnyOf { - us, err := r.UnwrapShape(item, history) - if err != nil { - return nil, StacktraceNewWrapped("union unwrap", err, base.Location, - stacktrace.WithPosition(&base.Position), stacktrace.WithType(stacktrace.TypeUnwrapping)) - } - *item = us + case *UnionShape: + if err := r.unwrapUnionShape(target.Base(), trg, history); err != nil { + return fmt.Errorf("unwrap union shape: %w", err) + } + } + return nil +} + +// UnwrapShape recursively copies and unwraps a shape. +// Note that this method removes information about links. +func (r *RAML) UnwrapShape(s *Shape, history []Shape) (Shape, error) { + if s == nil { + return nil, fmt.Errorf("shape is nil") + } + // Perform deep copy to avoid modifying the original shape + target := (*s).Clone() + + base := target.Base() + // Skip already unwrapped shapes + if base.IsUnwrapped() { + return target, nil + } + + for _, item := range history { + if item.Base().ID == base.ID { + base.Inherits = nil + return &RecursiveShape{BaseShape: *base, Head: &item}, nil } } + history = append(history, target) + + source, err := r.unwrapSourceIfObj(s, history) + if err != nil { + return nil, StacktraceNewWrapped("unwrap source if obj", err, base.Location, + stacktrace.WithPosition(&base.Position), stacktrace.WithType(stacktrace.TypeUnwrapping)) + } + + if errUnwrap := r.unwrapTarget(target, history); errUnwrap != nil { + return nil, StacktraceNewWrapped("unwrap target", errUnwrap, base.Location, + stacktrace.WithPosition(&base.Position), stacktrace.WithType(stacktrace.TypeUnwrapping)) + } for pair := base.CustomShapeFacetDefinitions.Oldest(); pair != nil; pair = pair.Next() { prop := pair.Value - us, err := r.UnwrapShape(prop.Shape, history) - if err != nil { - return nil, StacktraceNewWrapped("custom shape facet definition unwrap", err, base.Location, + us, errUnwrap := r.UnwrapShape(prop.Shape, history) + if errUnwrap != nil { + return nil, StacktraceNewWrapped("custom shape facet definition unwrap", errUnwrap, base.Location, stacktrace.WithPosition(&base.Position), stacktrace.WithType(stacktrace.TypeUnwrapping)) } *prop.Shape = us } if source != nil { - ms, err := r.Inherit(source, target) - if err != nil { - return nil, StacktraceNewWrapped("merge shapes", err, base.Location, + ms, errInherit := r.Inherit(source, target) + if errInherit != nil { + return nil, StacktraceNewWrapped("merge shapes", errInherit, base.Location, stacktrace.WithPosition(&base.Position), stacktrace.WithType(stacktrace.TypeUnwrapping)) } ms.Base().unwrapped = true diff --git a/validate.go b/validate.go index 0898d88..93b97ca 100644 --- a/validate.go +++ b/validate.go @@ -1,125 +1,125 @@ package raml -import "github.com/acronis/go-stacktrace" +import ( + "fmt" -func (r *RAML) ValidateShapes() error { - // Unwrap cache stores the mapping of original IDs to unwrapped shapes - // to ensure the original references (aliases and links) match. - unwrapCache := make(map[string]Shape) + "github.com/acronis/go-stacktrace" + orderedmap "github.com/wk8/go-ordered-map/v2" +) +func (r *RAML) validateTypes(types *orderedmap.OrderedMap[string, *Shape], + unwrapCache map[string]Shape) *stacktrace.StackTrace { var st *stacktrace.StackTrace + for pair := types.Oldest(); pair != nil; pair = pair.Next() { + shape := pair.Value + s := *shape + if !s.Base().unwrapped { + us, err := r.UnwrapShape(shape, make([]Shape, 0)) + if err != nil { + se := StacktraceNewWrapped("unwrap shape", err, s.Base().Location, + stacktrace.WithPosition(&s.Base().Position), + stacktrace.WithType(stacktrace.TypeValidating)) + if st == nil { + st = se + } else { + st = st.Append(se) + } + continue + } + unwrapCache[s.Base().ID] = s + s = us + } + if err := s.Check(); err != nil { + se := StacktraceNewWrapped("check type", err, s.Base().Location, + stacktrace.WithPosition(&s.Base().Position), + stacktrace.WithType(stacktrace.TypeValidating)) + if st == nil { + st = se + } else { + st = st.Append(se) + } + continue + } + if err := r.validateShapeCommons(s); err != nil { + se := StacktraceNewWrapped("validate shape commons", err, s.Base().Location, + stacktrace.WithPosition(&s.Base().Position), + stacktrace.WithType(stacktrace.TypeValidating)) + if st == nil { + st = se + } else { + st = st.Append(se) + } + continue + } + } + return st +} +func (r *RAML) validateLibrary(f *Library, unwrapCache map[string]Shape) *stacktrace.StackTrace { + st := r.validateTypes(f.AnnotationTypes, unwrapCache) + + if se := r.validateTypes(f.Types, unwrapCache); se != nil { + if st == nil { + st = se + } else { + st = st.Append(se) + } + } + return st +} + +func (r *RAML) validateDataType(f *DataType, unwrapCache map[string]Shape) *stacktrace.StackTrace { + s := *f.Shape + if !s.Base().unwrapped { + us, err := r.UnwrapShape(f.Shape, make([]Shape, 0)) + if err != nil { + return StacktraceNewWrapped("unwrap shape", err, s.Base().Location, + stacktrace.WithPosition(&s.Base().Position), + stacktrace.WithType(stacktrace.TypeValidating)) + } + unwrapCache[s.Base().ID] = s + s = us + } + if err := s.Check(); err != nil { + return StacktraceNewWrapped("check data type", err, s.Base().Location, + stacktrace.WithPosition(&s.Base().Position), + stacktrace.WithType(stacktrace.TypeValidating)) + } + if err := r.validateShapeCommons(s); err != nil { + return StacktraceNewWrapped("validate shape commons", err, s.Base().Location, + stacktrace.WithPosition(&s.Base().Position), + stacktrace.WithType(stacktrace.TypeValidating)) + } + return nil +} + +func (r *RAML) validateFragments(unwrapCache map[string]Shape) *stacktrace.StackTrace { + var st *stacktrace.StackTrace for _, frag := range r.fragmentsCache { switch f := frag.(type) { case *Library: - for pair := f.AnnotationTypes.Oldest(); pair != nil; pair = pair.Next() { - shape := pair.Value - s := *shape - if !s.Base().unwrapped { - us, err := r.UnwrapShape(shape, make([]Shape, 0)) - if err != nil { - se := StacktraceNewWrapped("unwrap shape", err, s.Base().Location, - stacktrace.WithPosition(&s.Base().Position), - stacktrace.WithType(stacktrace.TypeValidating)) - if st == nil { - st = se - } else { - st = st.Append(se) - } - continue - } - unwrapCache[s.Base().ID] = s - s = us - } - if err := s.Check(); err != nil { - se := StacktraceNewWrapped("check annotation type", err, s.Base().Location, - stacktrace.WithPosition(&s.Base().Position), - stacktrace.WithType(stacktrace.TypeValidating)) - if st == nil { - st = se - } else { - st = st.Append(se) - } - continue - } - if err := r.validateShapeCommons(s); err != nil { - se := StacktraceNewWrapped("validate shape commons", err, s.Base().Location, - stacktrace.WithPosition(&s.Base().Position), - stacktrace.WithType(stacktrace.TypeValidating)) - if st == nil { - st = se - } else { - st = st.Append(se) - } - continue - } - } - for pair := f.Types.Oldest(); pair != nil; pair = pair.Next() { - shape := pair.Value - s := *shape - if !s.Base().unwrapped { - us, err := r.UnwrapShape(shape, make([]Shape, 0)) - if err != nil { - se := StacktraceNewWrapped("unwrap shape", err, s.Base().Location, - stacktrace.WithPosition(&s.Base().Position), - stacktrace.WithType(stacktrace.TypeValidating)) - if st == nil { - st = se - } else { - st = st.Append(se) - } - continue - } - unwrapCache[s.Base().ID] = s - s = us - } - if err := s.Check(); err != nil { - se := StacktraceNewWrapped("check type", err, s.Base().Location, - stacktrace.WithPosition(&s.Base().Position), - stacktrace.WithType(stacktrace.TypeValidating)) - if st == nil { - st = se - } else { - st = st.Append(se) - } - continue - } - if err := r.validateShapeCommons(s); err != nil { - se := StacktraceNewWrapped("validate shape commons", err, s.Base().Location, - stacktrace.WithPosition(&s.Base().Position), - stacktrace.WithType(stacktrace.TypeValidating)) - if st == nil { - st = se - } else { - st = st.Append(se) - } - continue + if err := r.validateLibrary(f, unwrapCache); err != nil { + if st == nil { + st = err + } else { + st = st.Append(err) } } case *DataType: - s := *f.Shape - if !s.Base().unwrapped { - us, err := r.UnwrapShape(f.Shape, make([]Shape, 0)) - if err != nil { - return StacktraceNewWrapped("unwrap shape", err, s.Base().Location, - stacktrace.WithPosition(&s.Base().Position), - stacktrace.WithType(stacktrace.TypeValidating)) + if err := r.validateDataType(f, unwrapCache); err != nil { + if st == nil { + st = err + } else { + st = st.Append(err) } - unwrapCache[s.Base().ID] = s - s = us - } - if err := s.Check(); err != nil { - return StacktraceNewWrapped("check data type", err, s.Base().Location, - stacktrace.WithPosition(&s.Base().Position), - stacktrace.WithType(stacktrace.TypeValidating)) - } - if err := r.validateShapeCommons(s); err != nil { - return StacktraceNewWrapped("validate shape commons", err, s.Base().Location, - stacktrace.WithPosition(&s.Base().Position), - stacktrace.WithType(stacktrace.TypeValidating)) } } } + return st +} + +func (r *RAML) validateDomainExtensions(unwrapCache map[string]Shape) *stacktrace.StackTrace { + var st *stacktrace.StackTrace for _, item := range r.domainExtensions { db := *item.DefinedBy if !db.Base().unwrapped { @@ -149,12 +149,53 @@ func (r *RAML) ValidateShapes() error { continue } } + + return st +} + +func (r *RAML) ValidateShapes() error { + // Unwrap cache stores the mapping of original IDs to unwrapped shapes + // to ensure the original references (aliases and links) match. + unwrapCache := make(map[string]Shape) + + st := r.validateFragments(unwrapCache) + + if se := r.validateDomainExtensions(unwrapCache); se != nil { + if st == nil { + st = se + } else { + st = st.Append(se) + } + } + if st != nil { return st } return nil } +func (r *RAML) validateObjectShape(s *ObjectShape) error { + if s.Properties != nil { + for pair := s.Properties.Oldest(); pair != nil; pair = pair.Next() { + k, prop := pair.Key, pair.Value + s := *prop.Shape + if err := r.validateShapeCommons(s); err != nil { + return StacktraceNewWrapped("validate property", err, s.Base().Location, + stacktrace.WithPosition(&s.Base().Position), stacktrace.WithInfo("property", k)) + } + } + for pair := s.PatternProperties.Oldest(); pair != nil; pair = pair.Next() { + k, prop := pair.Key, pair.Value + s := *prop.Shape + if err := r.validateShapeCommons(s); err != nil { + return StacktraceNewWrapped("validate pattern property", err, s.Base().Location, + stacktrace.WithPosition(&s.Base().Position), stacktrace.WithInfo("property", k)) + } + } + } + return nil +} + func (r *RAML) validateShapeCommons(s Shape) error { if err := r.validateShapeFacets(s); err != nil { return err @@ -165,23 +206,8 @@ func (r *RAML) validateShapeCommons(s Shape) error { switch s := s.(type) { case *ObjectShape: - if s.Properties != nil { - for pair := s.Properties.Oldest(); pair != nil; pair = pair.Next() { - k, prop := pair.Key, pair.Value - s := *prop.Shape - if err := r.validateShapeCommons(s); err != nil { - return StacktraceNewWrapped("validate property", err, s.Base().Location, - stacktrace.WithPosition(&s.Base().Position), stacktrace.WithInfo("property", k)) - } - } - for pair := s.PatternProperties.Oldest(); pair != nil; pair = pair.Next() { - k, prop := pair.Key, pair.Value - s := *prop.Shape - if err := r.validateShapeCommons(s); err != nil { - return StacktraceNewWrapped("validate pattern property", err, s.Base().Location, - stacktrace.WithPosition(&s.Base().Position), stacktrace.WithInfo("property", k)) - } - } + if err := r.validateObjectShape(s); err != nil { + return fmt.Errorf("validate object shape: %w", err) } case *ArrayShape: if s.Items != nil {