diff --git a/changes/20260521144643.feature b/changes/20260521144643.feature new file mode 100644 index 0000000000..33966db8d1 --- /dev/null +++ b/changes/20260521144643.feature @@ -0,0 +1 @@ +:zap: add helpers for supporting `json` marshalling with better performance than `encoding/json` diff --git a/changes/20260521162032.feature b/changes/20260521162032.feature new file mode 100644 index 0000000000..c37b756b61 --- /dev/null +++ b/changes/20260521162032.feature @@ -0,0 +1 @@ +:sparkles: add helpers for serialising yaml diff --git a/changes/20260521183044.feature b/changes/20260521183044.feature new file mode 100644 index 0000000000..6ed5ae1a9f --- /dev/null +++ b/changes/20260521183044.feature @@ -0,0 +1 @@ +:sparkles: add helpers to validate files against a JSON schema in `jsonschema` diff --git a/changes/20260521184825.feature b/changes/20260521184825.feature new file mode 100644 index 0000000000..c88392cc80 --- /dev/null +++ b/changes/20260521184825.feature @@ -0,0 +1 @@ +:sparkles: `[filesystem]` Added validation rules for checking that a file path has the required extension diff --git a/utils/filesystem/filepath.go b/utils/filesystem/filepath.go index 7b849a6041..e7d91518c1 100644 --- a/utils/filesystem/filepath.go +++ b/utils/filesystem/filepath.go @@ -1,13 +1,9 @@ package filesystem import ( - "io/fs" "path" "path/filepath" "strings" - "syscall" - - validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/ARM-software/golang-utils/utils/commonerrors" "github.com/ARM-software/golang-utils/utils/platform" @@ -282,103 +278,3 @@ func EvalSymlinks(fs FS, pathWithSymlinks string) (populatedPath string, err err func EndsWithPathSeparator(fs FS, filePath string) bool { return strings.HasSuffix(filePath, "/") || strings.HasSuffix(filePath, string(fs.PathSeparator())) } - -// NewPathValidationRule returns a validation rule to use in configuration. -// The rule checks whether a string is a valid not empty path. -// `when` describes whether the rule is enforced or not -func NewPathValidationRule(filesystem FS, when bool) validation.Rule { - return &pathValidationRule{condition: when, filesystem: filesystem} -} - -// NewOSPathValidationRule returns a validation rule to use in configuration. -// The rule checks whether a string is a valid path for the Operating System's filesystem. -// `when` describes whether the rule is enforced or not -func NewOSPathValidationRule(when bool) validation.Rule { - return NewPathValidationRule(GetGlobalFileSystem(), when) -} - -type pathValidationRule struct { - condition bool - filesystem FS -} - -func (r *pathValidationRule) Validate(value interface{}) error { - err := validation.Required.When(r.condition).Validate(value) - if err != nil { - return commonerrors.WrapErrorf(commonerrors.ErrUndefined, err, "path [%v] is required", value) - } - if !r.condition { - return nil - } - pathString, err := validation.EnsureString(value) - if err != nil { - return commonerrors.WrapErrorf(commonerrors.ErrInvalid, err, "path [%v] must be a string", value) - } - pathString = strings.TrimSpace(pathString) - // This check is here because it validates the path on any platform (it is a cross-platform check) - // Indeed if the path exists, then it can only be valid. - if r.filesystem.Exists(pathString) { - return nil - } - - // Inspired from https://github.com/go-playground/validator/blob/84254aeb5a59e615ec0b66ab53b988bc0677f55e/baked_in.go#L1604 and https://stackoverflow.com/questions/35231846/golang-check-if-string-is-valid-path - if pathString == "" { - return commonerrors.Newf(commonerrors.ErrUndefined, "the path [%v] is empty", value) - } - // This check is to catch errors on Linux. It does not work as well on Windows. - if _, err := r.filesystem.Stat(pathString); err != nil { - switch t := err.(type) { - case *fs.PathError: - if t.Err == syscall.EINVAL { - return commonerrors.WrapErrorf(commonerrors.ErrInvalid, err, "the path [%v] has invalid characters", value) - } - default: - // make the linter happy - } - } - // The following case is not caught on Windows by the check above. - if strings.Contains(pathString, "\n") { - return commonerrors.Newf(commonerrors.ErrInvalid, "the path [%v] has carriage returns characters", value) - } - - // TODO add platform validation checks: e.g. https://learn.microsoft.com/en-gb/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN on windows - - return nil -} - -// NewPathExistRule returns a validation rule to use in configuration. -// The rule checks whether a string is a valid not empty path and actually exists. -// `when` describes whether the rule is enforced or not. -func NewPathExistRule(filesystem FS, when bool) validation.Rule { - return &pathExistValidationRule{filesystem: filesystem, condition: when} -} - -// NewOSPathExistRule returns a validation rule to use in configuration. -// The rule checks whether a string is a valid path for the Operating system's filesystem and actually exists. -// `when` describes whether the rule is enforced or not. -func NewOSPathExistRule(when bool) validation.Rule { - return NewPathExistRule(GetGlobalFileSystem(), when) -} - -type pathExistValidationRule struct { - condition bool - filesystem FS -} - -func (r *pathExistValidationRule) Validate(value interface{}) error { - err := NewPathValidationRule(r.filesystem, r.condition).Validate(value) - if err != nil { - return err - } - if !r.condition { - return nil - } - path, err := validation.EnsureString(value) - if err != nil { - return commonerrors.WrapErrorf(commonerrors.ErrInvalid, err, "path [%v] must be a string", value) - } - if !r.filesystem.Exists(path) { - err = commonerrors.Newf(commonerrors.ErrNotFound, "path [%v] does not exist", path) - } - return err -} diff --git a/utils/filesystem/filepath_test.go b/utils/filesystem/filepath_test.go index 3d9c076fdc..882557d8da 100644 --- a/utils/filesystem/filepath_test.go +++ b/utils/filesystem/filepath_test.go @@ -11,8 +11,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/ARM-software/golang-utils/utils/commonerrors" - "github.com/ARM-software/golang-utils/utils/commonerrors/errortest" "github.com/ARM-software/golang-utils/utils/platform" ) @@ -219,72 +217,6 @@ func TestEndsWithPathSeparator(t *testing.T) { } } -func TestNewPathExistRule(t *testing.T) { - t.Run("disable", func(t *testing.T) { - err := NewOSPathExistRule(false).Validate(faker.URL()) - require.NoError(t, err) - }) - t.Run("happy existing path", func(t *testing.T) { - require.NoError(t, NewOSPathExistRule(true).Validate(TempDirectory())) - testDir, err := TempDirInTempDir("test-path-rule-") - require.NoError(t, err) - defer func() { _ = Rm(testDir) }() - require.NoError(t, NewOSPathExistRule(true).Validate(testDir)) - testFile, err := TouchTempFile(testDir, "test-file*.test") - require.NoError(t, err) - require.NoError(t, NewOSPathExistRule(true).Validate(testFile)) - }) - t.Run("non-existent path but valid", func(t *testing.T) { - err := NewOSPathExistRule(true).Validate(strings.ReplaceAll(faker.Sentence(), " ", "/")) - require.Error(t, err) - errortest.AssertError(t, err, commonerrors.ErrNotFound) - err = NewOSPathValidationRule(true).Validate(strings.ReplaceAll(faker.Sentence(), " ", "/")) - require.NoError(t, err) - err = NewOSPathExistRule(true).Validate(faker.URL()) - require.Error(t, err) - errortest.AssertError(t, err, commonerrors.ErrNotFound) - err = NewOSPathValidationRule(true).Validate(faker.URL()) - require.NoError(t, err) - }) - - t.Run("invalid paths", func(t *testing.T) { - tests := []struct { - entry any - expectedError []error - }{ - { - entry: nil, - expectedError: []error{commonerrors.ErrUndefined, commonerrors.ErrInvalid}, - }, - { - entry: " ", - expectedError: []error{commonerrors.ErrUndefined, commonerrors.ErrInvalid}, - }, - { - entry: 123, - expectedError: []error{commonerrors.ErrInvalid}, - }, - { - entry: fmt.Sprintf("%v\n%v\n%v", faker.Paragraph(), faker.Paragraph(), faker.Sentence()), - expectedError: []error{commonerrors.ErrInvalid}, - }, - } - for i := range tests { - test := tests[i] - t.Run(fmt.Sprintf("%v", test.entry), func(t *testing.T) { - err := NewOSPathValidationRule(true).Validate(test.entry) - require.Error(t, err) - errortest.AssertError(t, err, test.expectedError...) - err = NewOSPathExistRule(true).Validate(test.entry) - require.Error(t, err) - errortest.AssertError(t, err, test.expectedError...) - }) - } - - }) - -} - func TestFilePathJoin(t *testing.T) { embedFS, err := NewEmbedFileSystem(&testContent) require.NoError(t, err) diff --git a/utils/filesystem/rules.go b/utils/filesystem/rules.go new file mode 100644 index 0000000000..4dc8c7d4b1 --- /dev/null +++ b/utils/filesystem/rules.go @@ -0,0 +1,208 @@ +package filesystem + +import ( + "io/fs" + "strings" + "syscall" + + validation "github.com/go-ozzo/ozzo-validation/v4" + + "github.com/ARM-software/golang-utils/utils/collection" + "github.com/ARM-software/golang-utils/utils/commonerrors" + "github.com/ARM-software/golang-utils/utils/reflection" +) + +type filesystemValidationRule struct { + condition bool + filesystem FS +} + +func (r *filesystemValidationRule) Validate(value any) error { + err := validation.Required.When(r.condition).Validate(value) + if err != nil { + return commonerrors.WrapErrorf(commonerrors.ErrUndefined, err, "missing value") + } + if r.filesystem == nil { + return commonerrors.UndefinedVariable("filesystem to consider") + } + return nil +} + +type pathValidationRule struct { + filesystemValidationRule +} + +func (r *pathValidationRule) Validate(value any) error { + err := r.filesystemValidationRule.Validate(value) + if err != nil { + return err + } + if !r.condition { + return nil + } + pathString, err := validation.EnsureString(value) + if err != nil { + return commonerrors.WrapErrorf(commonerrors.ErrInvalid, err, "path [%v] must be a string", value) + } + pathString = strings.TrimSpace(pathString) + // This check is here because it validates the path on any platform (it is a cross-platform check) + // Indeed if the path exists, then it can only be valid. + if r.filesystem.Exists(pathString) { + return nil + } + + // Inspired from https://github.com/go-playground/validator/blob/84254aeb5a59e615ec0b66ab53b988bc0677f55e/baked_in.go#L1604 and https://stackoverflow.com/questions/35231846/golang-check-if-string-is-valid-path + if pathString == "" { + return commonerrors.Newf(commonerrors.ErrUndefined, "the path [%v] is empty", value) + } + // This check is to catch errors on Linux. It does not work as well on Windows. + if _, err := r.filesystem.Stat(pathString); err != nil { + switch t := err.(type) { + case *fs.PathError: + if t.Err == syscall.EINVAL { + return commonerrors.WrapErrorf(commonerrors.ErrInvalid, err, "the path [%v] has invalid characters", value) + } + default: + // make the linter happy + } + } + // The following case is not caught on Windows by the check above. + if strings.Contains(pathString, "\n") { + return commonerrors.Newf(commonerrors.ErrInvalid, "the path [%v] has carriage returns characters", value) + } + + // TODO add platform validation checks: e.g. https://learn.microsoft.com/en-gb/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN on windows + + return nil +} + +// NewPathValidationRule returns a validation rule to use in configuration. +// The rule checks whether a string is a valid not empty path. +// `when` describes whether the rule is enforced or not. +func NewPathValidationRule(filesystem FS, when bool) validation.Rule { + return &pathValidationRule{ + filesystemValidationRule: filesystemValidationRule{ + condition: when, + filesystem: filesystem, + }, + } +} + +// NewOSPathValidationRule returns a validation rule to use in configuration. +// The rule checks whether a string is a valid path for the operating system's +// filesystem. +// `when` describes whether the rule is enforced or not. +func NewOSPathValidationRule(when bool) validation.Rule { + return NewPathValidationRule(GetGlobalFileSystem(), when) +} + +type pathExistValidationRule struct { + pathValidationRule +} + +func (r *pathExistValidationRule) Validate(value any) error { + err := r.pathValidationRule.Validate(value) + if err != nil { + return err + } + if !r.condition { + return nil + } + path, err := validation.EnsureString(value) + if err != nil { + return commonerrors.WrapErrorf(commonerrors.ErrInvalid, err, "path [%v] must be a string", value) + } + if !r.filesystem.Exists(path) { + err = commonerrors.Newf(commonerrors.ErrNotFound, "path [%v] does not exist", path) + } + return err +} + +// NewPathExistRule returns a validation rule to use in configuration. +// The rule checks whether a string is a valid not empty path and actually +// exists. +// `when` describes whether the rule is enforced or not. +func NewPathExistRule(filesystem FS, when bool) validation.Rule { + return &pathExistValidationRule{ + pathValidationRule: pathValidationRule{ + filesystemValidationRule: filesystemValidationRule{ + condition: when, + filesystem: filesystem, + }, + }, + } +} + +// NewOSPathExistRule returns a validation rule to use in configuration. +// The rule checks whether a string is a valid path for the operating system's +// filesystem and actually exists. +// `when` describes whether the rule is enforced or not. +func NewOSPathExistRule(when bool) validation.Rule { + return NewPathExistRule(GetGlobalFileSystem(), when) +} + +type pathExtensionValidationRule struct { + pathValidationRule + extensions []string +} + +// NewPathExtensionRule returns a validation rule that checks whether a path has +// an extension present in the supplied list. +// `when` describes whether the rule is enforced or not. +func NewPathExtensionRule(filesystem FS, when bool, extensions ...string) validation.Rule { + return &pathExtensionValidationRule{ + pathValidationRule: pathValidationRule{ + filesystemValidationRule: filesystemValidationRule{ + condition: when, + filesystem: filesystem, + }, + }, + extensions: normaliseExtensions(filesystem, extensions...), + } + +} + +// NewOSPathExtensionRule returns a validation rule that checks whether a path +// on the global filesystem has an extension present in the supplied list. +// `when` describes whether the rule is enforced or not. +func NewOSPathExtensionRule(when bool, extensions ...string) validation.Rule { + return NewPathExtensionRule(GetGlobalFileSystem(), when, extensions...) +} + +func (r *pathExtensionValidationRule) Validate(value any) error { + err := r.pathValidationRule.Validate(value) + if err != nil { + return err + } + if !r.condition { + return nil + } + if len(r.extensions) == 0 { + return commonerrors.UndefinedVariable("allowed file extensions") + } + + pathString, err := validation.EnsureString(value) + if err != nil { + return commonerrors.WrapErrorf(commonerrors.ErrInvalid, err, "path [%v] must be a string", value) + } + + extension := strings.ToLower(FilePathExt(r.filesystem, strings.TrimSpace(pathString))) + if reflection.IsEmpty(extension) { + return commonerrors.Newf(commonerrors.ErrNoExtension, "path [%v] has no extension", value) + } + if collection.In(r.extensions, extension, collection.StringMatch) { + return nil + } + return commonerrors.Newf(commonerrors.ErrInvalid, "path [%v] must have one of the extensions %v", value, r.extensions) + +} + +func normaliseExtensions(fs FS, extensions ...string) []string { + return collection.Map[string, string](extensions, func(extension string) string { + extension = strings.TrimSpace(extension) + if !strings.HasPrefix(extension, ".") { + extension = "." + extension + } + return strings.ToLower(FilePathClean(fs, extension)) + }) +} diff --git a/utils/filesystem/rules_test.go b/utils/filesystem/rules_test.go new file mode 100644 index 0000000000..6561b0166e --- /dev/null +++ b/utils/filesystem/rules_test.go @@ -0,0 +1,113 @@ +package filesystem + +import ( + "fmt" + "strings" + "testing" + + "github.com/go-faker/faker/v4" + "github.com/stretchr/testify/require" + + "github.com/ARM-software/golang-utils/utils/commonerrors" + "github.com/ARM-software/golang-utils/utils/commonerrors/errortest" +) + +func TestNewPathExistRule(t *testing.T) { + t.Run("disable", func(t *testing.T) { + err := NewOSPathExistRule(false).Validate(faker.URL()) + require.NoError(t, err) + }) + t.Run("happy existing path", func(t *testing.T) { + require.NoError(t, NewOSPathExistRule(true).Validate(TempDirectory())) + testDir, err := TempDirInTempDir("test-path-rule-") + require.NoError(t, err) + defer func() { _ = Rm(testDir) }() + require.NoError(t, NewOSPathExistRule(true).Validate(testDir)) + testFile, err := TouchTempFile(testDir, "test-file*.test") + require.NoError(t, err) + require.NoError(t, NewOSPathExistRule(true).Validate(testFile)) + }) + t.Run("non-existent path but valid", func(t *testing.T) { + err := NewOSPathExistRule(true).Validate(strings.ReplaceAll(faker.Sentence(), " ", "/")) + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrNotFound) + err = NewOSPathValidationRule(true).Validate(strings.ReplaceAll(faker.Sentence(), " ", "/")) + require.NoError(t, err) + err = NewOSPathExistRule(true).Validate(faker.URL()) + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrNotFound) + err = NewOSPathValidationRule(true).Validate(faker.URL()) + require.NoError(t, err) + }) + + t.Run("invalid paths", func(t *testing.T) { + tests := []struct { + entry any + expectedError []error + }{ + { + entry: nil, + expectedError: []error{commonerrors.ErrUndefined, commonerrors.ErrInvalid}, + }, + { + entry: " ", + expectedError: []error{commonerrors.ErrUndefined, commonerrors.ErrInvalid}, + }, + { + entry: 123, + expectedError: []error{commonerrors.ErrInvalid}, + }, + { + entry: fmt.Sprintf("%v\n%v\n%v", faker.Paragraph(), faker.Paragraph(), faker.Sentence()), + expectedError: []error{commonerrors.ErrInvalid}, + }, + } + for i := range tests { + test := tests[i] + t.Run(fmt.Sprintf("%v", test.entry), func(t *testing.T) { + err := NewOSPathValidationRule(true).Validate(test.entry) + require.Error(t, err) + errortest.AssertError(t, err, test.expectedError...) + err = NewOSPathExistRule(true).Validate(test.entry) + require.Error(t, err) + errortest.AssertError(t, err, test.expectedError...) + }) + } + + }) +} + +func TestNewPathExtensionRule(t *testing.T) { + t.Run("disable", func(t *testing.T) { + err := NewOSPathExtensionRule(false, ".json").Validate(faker.URL()) + require.NoError(t, err) + }) + + t.Run("happy path on global filesystem", func(t *testing.T) { + require.NoError(t, NewOSPathExtensionRule(true, ".json", "yaml").Validate("config.json")) + require.NoError(t, NewOSPathExtensionRule(true, ".json", "yaml").Validate("config.yaml")) + }) + + t.Run("happy path on custom filesystem", func(t *testing.T) { + fs := NewTestFilesystem(t, '/') + require.NoError(t, NewPathExtensionRule(fs, true, ".json", ".yaml").Validate("folder/config.yaml")) + }) + + t.Run("missing extension", func(t *testing.T) { + err := NewOSPathExtensionRule(true, ".json").Validate("config") + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrNoExtension) + }) + + t.Run("wrong extension", func(t *testing.T) { + err := NewOSPathExtensionRule(true, ".json").Validate("config.yaml") + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrInvalid) + }) + + t.Run("missing allowed extensions", func(t *testing.T) { + err := NewOSPathExtensionRule(true).Validate("config.json") + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrUndefined) + }) +} diff --git a/utils/go.mod b/utils/go.mod index b9ef4dbb8a..7c56aa3c9f 100644 --- a/utils/go.mod +++ b/utils/go.mod @@ -29,10 +29,13 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.8 github.com/iamacarpet/go-win64api v0.0.0-20240507095429-873e84e85847 github.com/joho/godotenv v1.5.1 + github.com/mailru/easyjson v0.7.7 github.com/mitchellh/go-homedir v1.1.0 github.com/perimeterx/marshmallow v1.1.5 + github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7 github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 github.com/rs/zerolog v1.35.1 + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 github.com/sasha-s/go-deadlock v0.3.9 github.com/shirou/gopsutil/v4 v4.26.4 github.com/sirupsen/logrus v1.9.4 @@ -54,6 +57,7 @@ require ( golang.org/x/sync v0.20.0 golang.org/x/sys v0.45.0 golang.org/x/text v0.37.0 + sigs.k8s.io/yaml v1.6.0 ) require ( @@ -85,7 +89,6 @@ require ( github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect - github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/pascaldekloe/name v1.0.0 // indirect @@ -107,6 +110,7 @@ require ( github.com/yusufpapurcu/wmi v1.2.4 // indirect go.mongodb.org/mongo-driver v1.17.4 // indirect go.uber.org/multierr v1.10.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/tools v0.44.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect @@ -115,5 +119,15 @@ require ( tool ( github.com/dmarkham/enumer + github.com/mailru/easyjson/bootstrap + github.com/mailru/easyjson/buffer + github.com/mailru/easyjson/easyjson + github.com/mailru/easyjson/gen + github.com/mailru/easyjson/jlexer + github.com/mailru/easyjson/jwriter + github.com/mailru/easyjson/opt + github.com/mailru/easyjson/parser + github.com/mailru/easyjson/tests + github.com/pquerna/ffjson go.uber.org/mock/mockgen ) diff --git a/utils/go.sum b/utils/go.sum index a65b3cfe23..e29498ee2c 100644 --- a/utils/go.sum +++ b/utils/go.sum @@ -40,6 +40,8 @@ github.com/deckarep/golang-set/v2 v2.9.0 h1:prva4eP9UysWagLyKrtn074ughi0NnkIf0A4 github.com/deckarep/golang-set/v2 v2.9.0/go.mod h1:EWknQXbs0mcFpat2QOoXV0Ee57cD+w6ZEN76BR2JVrM= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dmarkham/enumer v1.6.3 h1:B4aV4OsfzbrS5rvjILt4mMjiWBA//cKxJUMsvHZ8mEI= github.com/dmarkham/enumer v1.6.3/go.mod h1:DyjXaqCglj4GhELF73oWiparNkYkXvmOBLza/o4kO74= github.com/dolmen-go/contextio v1.0.0 h1:bNfCo4gsRIhMeo6Z1ImXzkxZG81B6I5t2fUFJjphdAU= @@ -205,6 +207,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7 h1:xoIK0ctDddBMnc74udxJYBqlo9Ylnsp1waqjLsnef20= +github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7/go.mod h1:YARuvh7BUWHNhzDq2OM5tzR2RiCcN2D7sapiKyCel/M= github.com/rickb777/date v1.14.2/go.mod h1:swmf05C+hN+m8/Xh7gEq3uB6QJDNc5pQBWojKdHetOs= github.com/rickb777/plural v1.2.2/go.mod h1:xyHbelv4YvJE51gjMnHvk+U2e9zIysg6lTnSQK8XUYA= github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo= @@ -216,6 +220,8 @@ github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sasha-s/go-deadlock v0.3.9 h1:fiaT9rB7g5sr5ddNZvlwheclN9IP86eFW9WgqlEQV+w= github.com/sasha-s/go-deadlock v0.3.9/go.mod h1:KuZj51ZFmx42q/mPaYbRk0P1xcwe697zsJKE03vD4/Y= github.com/scjalliance/comshim v0.0.0-20190308082608-cf06d2532c4e h1:+/AzLkOdIXEPrAQtwAeWOBnPQ0BnYlBW0aCZmSb47u4= @@ -275,6 +281,8 @@ go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -361,3 +369,5 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/utils/mocks/mock_jsonschema.go b/utils/mocks/mock_jsonschema.go new file mode 100644 index 0000000000..4c56e34968 --- /dev/null +++ b/utils/mocks/mock_jsonschema.go @@ -0,0 +1,112 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ARM-software/golang-utils/utils/validation/jsonschema (interfaces: ISchemaValidator) +// +// Generated by this command: +// +// mockgen -destination=../../mocks/mock_jsonschema.go -package=mocks github.com/ARM-software/golang-utils/utils/validation/jsonschema ISchemaValidator +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + filesystem "github.com/ARM-software/golang-utils/utils/filesystem" + gomock "go.uber.org/mock/gomock" +) + +// MockISchemaValidator is a mock of ISchemaValidator interface. +type MockISchemaValidator struct { + ctrl *gomock.Controller + recorder *MockISchemaValidatorMockRecorder + isgomock struct{} +} + +// MockISchemaValidatorMockRecorder is the mock recorder for MockISchemaValidator. +type MockISchemaValidatorMockRecorder struct { + mock *MockISchemaValidator +} + +// NewMockISchemaValidator creates a new mock instance. +func NewMockISchemaValidator(ctrl *gomock.Controller) *MockISchemaValidator { + mock := &MockISchemaValidator{ctrl: ctrl} + mock.recorder = &MockISchemaValidatorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockISchemaValidator) EXPECT() *MockISchemaValidatorMockRecorder { + return m.recorder +} + +// ValidateContent mocks base method. +func (m *MockISchemaValidator) ValidateContent(arg0 context.Context, arg1 any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidateContent", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// ValidateContent indicates an expected call of ValidateContent. +func (mr *MockISchemaValidatorMockRecorder) ValidateContent(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateContent", reflect.TypeOf((*MockISchemaValidator)(nil).ValidateContent), arg0, arg1) +} + +// ValidateFile mocks base method. +func (m *MockISchemaValidator) ValidateFile(ctx context.Context, filepath string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidateFile", ctx, filepath) + ret0, _ := ret[0].(error) + return ret0 +} + +// ValidateFile indicates an expected call of ValidateFile. +func (mr *MockISchemaValidatorMockRecorder) ValidateFile(ctx, filepath any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateFile", reflect.TypeOf((*MockISchemaValidator)(nil).ValidateFile), ctx, filepath) +} + +// ValidateFileInFS mocks base method. +func (m *MockISchemaValidator) ValidateFileInFS(ctx context.Context, fs filesystem.FS, filepath string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidateFileInFS", ctx, fs, filepath) + ret0, _ := ret[0].(error) + return ret0 +} + +// ValidateFileInFS indicates an expected call of ValidateFileInFS. +func (mr *MockISchemaValidatorMockRecorder) ValidateFileInFS(ctx, fs, filepath any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateFileInFS", reflect.TypeOf((*MockISchemaValidator)(nil).ValidateFileInFS), ctx, fs, filepath) +} + +// ValidateFileInFSWithLimits mocks base method. +func (m *MockISchemaValidator) ValidateFileInFSWithLimits(ctx context.Context, fs filesystem.FS, filepath string, fileLimits filesystem.ILimits) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidateFileInFSWithLimits", ctx, fs, filepath, fileLimits) + ret0, _ := ret[0].(error) + return ret0 +} + +// ValidateFileInFSWithLimits indicates an expected call of ValidateFileInFSWithLimits. +func (mr *MockISchemaValidatorMockRecorder) ValidateFileInFSWithLimits(ctx, fs, filepath, fileLimits any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateFileInFSWithLimits", reflect.TypeOf((*MockISchemaValidator)(nil).ValidateFileInFSWithLimits), ctx, fs, filepath, fileLimits) +} + +// ValidateFileWithLimits mocks base method. +func (m *MockISchemaValidator) ValidateFileWithLimits(ctx context.Context, filepath string, fileLimits filesystem.ILimits) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidateFileWithLimits", ctx, filepath, fileLimits) + ret0, _ := ret[0].(error) + return ret0 +} + +// ValidateFileWithLimits indicates an expected call of ValidateFileWithLimits. +func (mr *MockISchemaValidatorMockRecorder) ValidateFileWithLimits(ctx, filepath, fileLimits any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateFileWithLimits", reflect.TypeOf((*MockISchemaValidator)(nil).ValidateFileWithLimits), ctx, filepath, fileLimits) +} diff --git a/utils/reflection/reflection.go b/utils/reflection/reflection.go index b19926c119..8a350bd16d 100644 --- a/utils/reflection/reflection.go +++ b/utils/reflection/reflection.go @@ -196,6 +196,12 @@ func IsEmpty(value any) bool { return valueUtils.IsEmpty(value) } +// IsNilInterface checks whether an interface value is nil even when it has been +// passed around as `any`. +func IsNilInterface(i any) bool { + return valueUtils.IsNilInterface(i) +} + // ToStructPtr returns an instance of the pointer (interface) to the object obj. func ToStructPtr(obj reflect.Value) (val any, err error) { if !obj.IsValid() { diff --git a/utils/serialization/json/decoder.go b/utils/serialization/json/decoder.go new file mode 100644 index 0000000000..3ad2082c5d --- /dev/null +++ b/utils/serialization/json/decoder.go @@ -0,0 +1,48 @@ +package json + +import ( + "context" + "io" + + "github.com/mailru/easyjson" + "github.com/pquerna/ffjson/ffjson" + + "github.com/ARM-software/golang-utils/utils/safeio" +) + +// Decoder decodes successive JSON values from a reader. +// It matches encoding/json.Decoder but wraps the supplied reader so reads obey +// context cancellation, and it uses generated fast JSON decoders when the +// target type supports those non-reflective decoding paths. +// This should not be used by more than one goroutine at a time. +type Decoder struct { + reader io.Reader +} + +// NewDecoder creates a Decoder that reads JSON values from r. +// It matches encoding/json.NewDecoder. +// +// It keeps the familiar Decoder workflow while adding context-aware reads and +// automatic use of generated fast JSON deserialisers. Examples of supported +// generators are github.com/mailru/easyjson and github.com/pquerna/ffjson. +func NewDecoder(ctx context.Context, r io.Reader) *Decoder { + return &Decoder{reader: safeio.NewContextualReader(ctx, r)} +} + +// Decode reads the next JSON value from the decoder into v. +// It matches encoding/json.Decoder.Decode. +// +// If v implements a generated fast JSON unmarshaler interface, Decode uses +// that non-reflective path. Otherwise, it falls back to the regular runtime +// path available through the supported fast JSON backend. The goal is to +// preserve a standard-library style API while making the faster implementation +// an internal detail. Examples of supported generators are +// github.com/mailru/easyjson and github.com/pquerna/ffjson. +func (d *Decoder) Decode(v any) error { + fastUnmarshaller, ok := v.(easyjson.Unmarshaler) + if ok { + return easyjson.UnmarshalFromReader(d.reader, fastUnmarshaller) + } + + return ffjson.NewDecoder().DecodeReader(d.reader, v) +} diff --git a/utils/serialization/json/encoder.go b/utils/serialization/json/encoder.go new file mode 100644 index 0000000000..5e0d9adba1 --- /dev/null +++ b/utils/serialization/json/encoder.go @@ -0,0 +1,51 @@ +package json + +import ( + "context" + "io" + + "github.com/mailru/easyjson" + "github.com/pquerna/ffjson/ffjson" + + "github.com/ARM-software/golang-utils/utils/safeio" +) + +// Encoder writes successive JSON values to a writer. +// It matches encoding/json.Encoder but wraps the supplied writer so writes obey +// context cancellation, and it uses generated fast JSON encoders when the +// value supports those non-reflective encoding paths. +// It allows encoding many objects to a single writer. +// This should not be used by more than one goroutine at a time. +type Encoder struct { + writer io.Writer +} + +// NewEncoder creates an Encoder that writes JSON values to w. +// It matches encoding/json.NewEncoder. +// +// It keeps the standard Encoder pattern while adding context-aware writes and +// automatic use of generated fast JSON serialisers when a type provides them. +// Examples of supported generators are github.com/mailru/easyjson and +// github.com/pquerna/ffjson. +func NewEncoder(ctx context.Context, w io.Writer) *Encoder { + return &Encoder{writer: safeio.ContextualWriter(ctx, w)} +} + +// Encode writes v as the next JSON value to the encoder's writer. +// It matches encoding/json.Encoder.Encode. +// +// If v implements a generated fast JSON marshaler interface, Encode uses that +// non-reflective path. Otherwise, it falls back to the regular runtime path +// available through the supported fast JSON backend. This preserves a familiar +// API for callers while making fast serialisers an internal optimisation +// instead of a caller concern. Examples of supported generators are +// github.com/mailru/easyjson and github.com/pquerna/ffjson. +func (e *Encoder) Encode(v any) error { + fastMarshaller, ok := v.(easyjson.Marshaler) + if ok { + _, err := easyjson.MarshalToWriter(fastMarshaller, e.writer) + return err + } + + return ffjson.NewEncoder(e.writer).Encode(v) +} diff --git a/utils/serialization/json/helpers.go b/utils/serialization/json/helpers.go new file mode 100644 index 0000000000..64456ec798 --- /dev/null +++ b/utils/serialization/json/helpers.go @@ -0,0 +1,100 @@ +// Package json mirrors the standard library's encoding/json helpers while +// adding two service-focused behaviours: +// - stream helpers are context aware, so long reads and writes can be +// cancelled through the supplied context +// - marshal and unmarshal operations automatically use generated fast JSON +// encoders and decoders when a type exposes those interfaces +// +// The intent is to keep call sites as close as possible to encoding/json while +// letting services benefit from non-reflective JSON code generation without +// each caller having to know which implementation a type uses. Examples of the +// supported fast-path libraries are github.com/mailru/easyjson and +// github.com/pquerna/ffjson. +package json + +import ( + "bytes" + "context" + + "github.com/mailru/easyjson" + "github.com/pquerna/ffjson/ffjson" + sigsyaml "sigs.k8s.io/yaml" + + "github.com/ARM-software/golang-utils/utils/commonerrors" + "github.com/ARM-software/golang-utils/utils/reflection" +) + +var nullBytes = []byte("null") + +// Marshal encodes a value to a JSON byte slice. +// It matches encoding/json.Marshal. +// +// If the value implements a generated fast JSON marshaler interface, that +// non-reflective path is used. Otherwise, it falls back to the regular runtime +// path available through the supported fast JSON backend. The rationale is to +// keep one familiar helper for callers while automatically taking a faster +// serialisation path when generated code exists. Examples of supported +// generators are github.com/mailru/easyjson and github.com/pquerna/ffjson. +func Marshal(v any) ([]byte, error) { + if reflection.IsNilInterface(v) { + return nullBytes, nil + } + + fastMarshaller, ok := v.(easyjson.Marshaler) + if ok { + return easyjson.Marshal(fastMarshaller) + } + + return ffjson.Marshal(v) +} + +// MarshalWithContext encodes a value to a JSON byte slice using a context-aware +// Encoder. +// It follows the same helper shape as Marshal, but routes the write through the +// package's context-aware streaming helpers. +func MarshalWithContext(ctx context.Context, v any) (content []byte, err error) { + var buf bytes.Buffer + encoder := NewEncoder(ctx, &buf) + err = encoder.Encode(v) + if err != nil { + return + } + content = buf.Bytes() + return +} + +// Unmarshal decodes a JSON byte slice into a destination value. +// It matches encoding/json.Unmarshal. +// +// If the destination implements a generated fast JSON unmarshaler interface, +// that non-reflective path is used. Otherwise it falls back to the regular +// runtime path available through the supported fast JSON backend. This lets +// calling code stay implementation-agnostic while types that opt into +// generated JSON code get faster deserialisation automatically. Examples of +// supported generators are github.com/mailru/easyjson and +// github.com/pquerna/ffjson. +func Unmarshal(data []byte, v any) error { + fastUnmarshaller, ok := v.(easyjson.Unmarshaler) + if ok { + return easyjson.Unmarshal(data, fastUnmarshaller) + } + + return ffjson.Unmarshal(data, v) +} + +// UnmarshallWithContext decodes a JSON byte slice into a destination value +// using a context-aware Decoder. +// It follows the same helper shape as Unmarshal, but routes the read through +// the package's context-aware streaming helpers. +func UnmarshallWithContext(ctx context.Context, data []byte, v any) error { + return NewDecoder(ctx, bytes.NewReader(data)).Decode(v) +} + +// ToYAML converts JSON data to YAML. +func ToYAML(rawJSON []byte) (yaml []byte, err error) { + yaml, err = sigsyaml.JSONToYAML(rawJSON) + if err != nil { + err = commonerrors.WrapError(commonerrors.ErrMarshalling, err, "failed converting JSON to YAML") + } + return +} diff --git a/utils/serialization/json/helpers_test.go b/utils/serialization/json/helpers_test.go new file mode 100644 index 0000000000..a1d876f741 --- /dev/null +++ b/utils/serialization/json/helpers_test.go @@ -0,0 +1,95 @@ +package json + +import ( + "bytes" + "context" + "testing" + + "github.com/go-faker/faker/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ARM-software/golang-utils/utils/commonerrors" + "github.com/ARM-software/golang-utils/utils/commonerrors/errortest" +) + +type TestStruct struct { + Elem1 string + Elem2 int64 + Elem3 string + Elem4 bool + Elem5 float64 +} + +func TestMarshalling(t *testing.T) { + test := TestStruct{ + Elem1: faker.Sentence(), + Elem2: 456, + Elem3: faker.Paragraph(), + Elem4: true, + Elem5: 0.415465464565464, + } + + encoded, err := Marshal(&test) + require.NoError(t, err) + + var decoded TestStruct + err = Unmarshal(encoded, &decoded) + require.NoError(t, err) + assert.Equal(t, test, decoded) +} + +func TestMarshallingEncoding(t *testing.T) { + test := TestStruct{ + Elem1: faker.Sentence(), + Elem2: 456, + Elem3: faker.Paragraph(), + Elem4: true, + Elem5: 0.415465545454464565464, + } + + var buf bytes.Buffer + encoder := NewEncoder(context.Background(), &buf) + err := encoder.Encode(test) + require.NoError(t, err) + + var decoded TestStruct + decoder := NewDecoder(context.Background(), &buf) + err = decoder.Decode(&decoded) + require.NoError(t, err) + assert.Equal(t, test, decoded) +} + +func TestMarshalAndUnmarshallWithContext(t *testing.T) { + test := TestStruct{ + Elem1: faker.Sentence(), + Elem2: 456, + Elem3: faker.Paragraph(), + Elem4: true, + Elem5: 0.415465464565464, + } + + encoded, err := MarshalWithContext(context.Background(), &test) + require.NoError(t, err) + + var decoded TestStruct + err = UnmarshallWithContext(context.Background(), encoded, &decoded) + require.NoError(t, err) + assert.Equal(t, test, decoded) +} + +func TestToYAML(t *testing.T) { + input := []byte(`{"name":"value","count":2}`) + + output, err := ToYAML(input) + require.NoError(t, err) + + assert.Contains(t, string(output), "name: value") + assert.Contains(t, string(output), "count: 2") +} + +func TestToYAMLInvalidJSON(t *testing.T) { + _, err := ToYAML([]byte(`{"name":`)) + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrMarshalling) +} diff --git a/utils/serialization/json/jsontest/easyjson/testing_types.go b/utils/serialization/json/jsontest/easyjson/testing_types.go new file mode 100644 index 0000000000..a77f0f0697 --- /dev/null +++ b/utils/serialization/json/jsontest/easyjson/testing_types.go @@ -0,0 +1,138 @@ +package easyjson + +import ( + "slices" + "time" + + "github.com/ARM-software/golang-utils/utils/collection" +) + +//go:generate go tool easyjson -all $GOFILE + +type TestingStruct struct { + Int int + Int8 int8 + Int16 int16 + Int32 int32 + Int64 int64 + String string + Bool bool + Duration time.Duration + SString []string + SInt []int + SInt8 []int8 + SInt16 []int16 + SInt32 []int32 + SInt64 []int64 + SFloat32 []float32 + SFloat64 []float64 + SBool []bool + Struct AStruct +} + +func (s *TestingStruct) Equals(other any) bool { + if s == other { + return true + } + + if other == nil { + return s == nil + } + + if o, ok := other.(*TestingStruct); ok { + return collection.All([]bool{ + s.Int == o.Int, + s.Int8 == o.Int8, + s.Int16 == o.Int16, + s.Int32 == o.Int32, + s.Int64 == o.Int64, + s.String == o.String, + s.Bool == o.Bool, + s.Duration == o.Duration, + slices.Equal(s.SString, o.SString), + slices.Equal(s.SInt, o.SInt), + slices.Equal(s.SInt8, o.SInt8), + slices.Equal(s.SInt16, o.SInt16), + slices.Equal(s.SInt32, o.SInt32), + slices.Equal(s.SInt64, o.SInt64), + slices.Equal(s.SFloat32, o.SFloat32), + slices.Equal(s.SFloat64, o.SFloat64), + slices.Equal(s.SBool, o.SBool), + s.Struct.Equals(o.Struct), + }) + } + + if o, ok := other.(TestingStruct); ok { + return collection.All([]bool{ + s.Int == o.Int, + s.Int8 == o.Int8, + s.Int16 == o.Int16, + s.Int32 == o.Int32, + s.Int64 == o.Int64, + s.String == o.String, + s.Bool == o.Bool, + slices.Equal(s.SString, o.SString), + slices.Equal(s.SInt, o.SInt), + slices.Equal(s.SInt8, o.SInt8), + slices.Equal(s.SInt16, o.SInt16), + slices.Equal(s.SInt32, o.SInt32), + slices.Equal(s.SInt64, o.SInt64), + slices.Equal(s.SFloat32, o.SFloat32), + slices.Equal(s.SFloat64, o.SFloat64), + slices.Equal(s.SBool, o.SBool), + s.Struct.Equals(o.Struct), + }) + } + + return false +} + +type AStruct struct { + Number int64 + Height int64 + AnotherStruct BStruct +} + +func (s *AStruct) Equals(other any) bool { + if s == other { + return true + } + + if other == nil { + return s == nil + } + + if o, ok := other.(*AStruct); ok { + return s.Number == o.Number && s.Height == o.Height && s.AnotherStruct.Equals(&o.AnotherStruct) + } + + if o, ok := other.(AStruct); ok { + return s.Number == o.Number && s.Height == o.Height && s.AnotherStruct.Equals(&o.AnotherStruct) + } + + return false +} + +type BStruct struct { + Image string +} + +func (s *BStruct) Equals(other any) bool { + if s == other { + return true + } + + if other == nil { + return s == nil + } + + if o, ok := other.(*BStruct); ok { + return s.Image == o.Image + } + + if o, ok := other.(BStruct); ok { + return s.Image == o.Image + } + + return false +} diff --git a/utils/serialization/json/jsontest/easyjson/testing_types_easyjson.go b/utils/serialization/json/jsontest/easyjson/testing_types_easyjson.go new file mode 100644 index 0000000000..8b50fe392c --- /dev/null +++ b/utils/serialization/json/jsontest/easyjson/testing_types_easyjson.go @@ -0,0 +1,639 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package easyjson + +import ( + json "encoding/json" + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" + time "time" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjson84e285b8DecodeGithubComARMSoftwareGolangUtilsUtilsSerializationJsonJsontestEasyjson(in *jlexer.Lexer, out *TestingStruct) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "Int": + out.Int = int(in.Int()) + case "Int8": + out.Int8 = int8(in.Int8()) + case "Int16": + out.Int16 = int16(in.Int16()) + case "Int32": + out.Int32 = int32(in.Int32()) + case "Int64": + out.Int64 = int64(in.Int64()) + case "String": + out.String = string(in.String()) + case "Bool": + out.Bool = bool(in.Bool()) + case "Duration": + out.Duration = time.Duration(in.Int64()) + case "SString": + if in.IsNull() { + in.Skip() + out.SString = nil + } else { + in.Delim('[') + if out.SString == nil { + if !in.IsDelim(']') { + out.SString = make([]string, 0, 4) + } else { + out.SString = []string{} + } + } else { + out.SString = (out.SString)[:0] + } + for !in.IsDelim(']') { + var v1 string + v1 = string(in.String()) + out.SString = append(out.SString, v1) + in.WantComma() + } + in.Delim(']') + } + case "SInt": + if in.IsNull() { + in.Skip() + out.SInt = nil + } else { + in.Delim('[') + if out.SInt == nil { + if !in.IsDelim(']') { + out.SInt = make([]int, 0, 8) + } else { + out.SInt = []int{} + } + } else { + out.SInt = (out.SInt)[:0] + } + for !in.IsDelim(']') { + var v2 int + v2 = int(in.Int()) + out.SInt = append(out.SInt, v2) + in.WantComma() + } + in.Delim(']') + } + case "SInt8": + if in.IsNull() { + in.Skip() + out.SInt8 = nil + } else { + in.Delim('[') + if out.SInt8 == nil { + if !in.IsDelim(']') { + out.SInt8 = make([]int8, 0, 64) + } else { + out.SInt8 = []int8{} + } + } else { + out.SInt8 = (out.SInt8)[:0] + } + for !in.IsDelim(']') { + var v3 int8 + v3 = int8(in.Int8()) + out.SInt8 = append(out.SInt8, v3) + in.WantComma() + } + in.Delim(']') + } + case "SInt16": + if in.IsNull() { + in.Skip() + out.SInt16 = nil + } else { + in.Delim('[') + if out.SInt16 == nil { + if !in.IsDelim(']') { + out.SInt16 = make([]int16, 0, 32) + } else { + out.SInt16 = []int16{} + } + } else { + out.SInt16 = (out.SInt16)[:0] + } + for !in.IsDelim(']') { + var v4 int16 + v4 = int16(in.Int16()) + out.SInt16 = append(out.SInt16, v4) + in.WantComma() + } + in.Delim(']') + } + case "SInt32": + if in.IsNull() { + in.Skip() + out.SInt32 = nil + } else { + in.Delim('[') + if out.SInt32 == nil { + if !in.IsDelim(']') { + out.SInt32 = make([]int32, 0, 16) + } else { + out.SInt32 = []int32{} + } + } else { + out.SInt32 = (out.SInt32)[:0] + } + for !in.IsDelim(']') { + var v5 int32 + v5 = int32(in.Int32()) + out.SInt32 = append(out.SInt32, v5) + in.WantComma() + } + in.Delim(']') + } + case "SInt64": + if in.IsNull() { + in.Skip() + out.SInt64 = nil + } else { + in.Delim('[') + if out.SInt64 == nil { + if !in.IsDelim(']') { + out.SInt64 = make([]int64, 0, 8) + } else { + out.SInt64 = []int64{} + } + } else { + out.SInt64 = (out.SInt64)[:0] + } + for !in.IsDelim(']') { + var v6 int64 + v6 = int64(in.Int64()) + out.SInt64 = append(out.SInt64, v6) + in.WantComma() + } + in.Delim(']') + } + case "SFloat32": + if in.IsNull() { + in.Skip() + out.SFloat32 = nil + } else { + in.Delim('[') + if out.SFloat32 == nil { + if !in.IsDelim(']') { + out.SFloat32 = make([]float32, 0, 16) + } else { + out.SFloat32 = []float32{} + } + } else { + out.SFloat32 = (out.SFloat32)[:0] + } + for !in.IsDelim(']') { + var v7 float32 + v7 = float32(in.Float32()) + out.SFloat32 = append(out.SFloat32, v7) + in.WantComma() + } + in.Delim(']') + } + case "SFloat64": + if in.IsNull() { + in.Skip() + out.SFloat64 = nil + } else { + in.Delim('[') + if out.SFloat64 == nil { + if !in.IsDelim(']') { + out.SFloat64 = make([]float64, 0, 8) + } else { + out.SFloat64 = []float64{} + } + } else { + out.SFloat64 = (out.SFloat64)[:0] + } + for !in.IsDelim(']') { + var v8 float64 + v8 = float64(in.Float64()) + out.SFloat64 = append(out.SFloat64, v8) + in.WantComma() + } + in.Delim(']') + } + case "SBool": + if in.IsNull() { + in.Skip() + out.SBool = nil + } else { + in.Delim('[') + if out.SBool == nil { + if !in.IsDelim(']') { + out.SBool = make([]bool, 0, 64) + } else { + out.SBool = []bool{} + } + } else { + out.SBool = (out.SBool)[:0] + } + for !in.IsDelim(']') { + var v9 bool + v9 = bool(in.Bool()) + out.SBool = append(out.SBool, v9) + in.WantComma() + } + in.Delim(']') + } + case "Struct": + (out.Struct).UnmarshalEasyJSON(in) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson84e285b8EncodeGithubComARMSoftwareGolangUtilsUtilsSerializationJsonJsontestEasyjson(out *jwriter.Writer, in TestingStruct) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"Int\":" + out.RawString(prefix[1:]) + out.Int(int(in.Int)) + } + { + const prefix string = ",\"Int8\":" + out.RawString(prefix) + out.Int8(int8(in.Int8)) + } + { + const prefix string = ",\"Int16\":" + out.RawString(prefix) + out.Int16(int16(in.Int16)) + } + { + const prefix string = ",\"Int32\":" + out.RawString(prefix) + out.Int32(int32(in.Int32)) + } + { + const prefix string = ",\"Int64\":" + out.RawString(prefix) + out.Int64(int64(in.Int64)) + } + { + const prefix string = ",\"String\":" + out.RawString(prefix) + out.String(string(in.String)) + } + { + const prefix string = ",\"Bool\":" + out.RawString(prefix) + out.Bool(bool(in.Bool)) + } + { + const prefix string = ",\"Duration\":" + out.RawString(prefix) + out.Int64(int64(in.Duration)) + } + { + const prefix string = ",\"SString\":" + out.RawString(prefix) + if in.SString == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v10, v11 := range in.SString { + if v10 > 0 { + out.RawByte(',') + } + out.String(string(v11)) + } + out.RawByte(']') + } + } + { + const prefix string = ",\"SInt\":" + out.RawString(prefix) + if in.SInt == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v12, v13 := range in.SInt { + if v12 > 0 { + out.RawByte(',') + } + out.Int(int(v13)) + } + out.RawByte(']') + } + } + { + const prefix string = ",\"SInt8\":" + out.RawString(prefix) + if in.SInt8 == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v14, v15 := range in.SInt8 { + if v14 > 0 { + out.RawByte(',') + } + out.Int8(int8(v15)) + } + out.RawByte(']') + } + } + { + const prefix string = ",\"SInt16\":" + out.RawString(prefix) + if in.SInt16 == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v16, v17 := range in.SInt16 { + if v16 > 0 { + out.RawByte(',') + } + out.Int16(int16(v17)) + } + out.RawByte(']') + } + } + { + const prefix string = ",\"SInt32\":" + out.RawString(prefix) + if in.SInt32 == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v18, v19 := range in.SInt32 { + if v18 > 0 { + out.RawByte(',') + } + out.Int32(int32(v19)) + } + out.RawByte(']') + } + } + { + const prefix string = ",\"SInt64\":" + out.RawString(prefix) + if in.SInt64 == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v20, v21 := range in.SInt64 { + if v20 > 0 { + out.RawByte(',') + } + out.Int64(int64(v21)) + } + out.RawByte(']') + } + } + { + const prefix string = ",\"SFloat32\":" + out.RawString(prefix) + if in.SFloat32 == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v22, v23 := range in.SFloat32 { + if v22 > 0 { + out.RawByte(',') + } + out.Float32(float32(v23)) + } + out.RawByte(']') + } + } + { + const prefix string = ",\"SFloat64\":" + out.RawString(prefix) + if in.SFloat64 == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v24, v25 := range in.SFloat64 { + if v24 > 0 { + out.RawByte(',') + } + out.Float64(float64(v25)) + } + out.RawByte(']') + } + } + { + const prefix string = ",\"SBool\":" + out.RawString(prefix) + if in.SBool == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v26, v27 := range in.SBool { + if v26 > 0 { + out.RawByte(',') + } + out.Bool(bool(v27)) + } + out.RawByte(']') + } + } + { + const prefix string = ",\"Struct\":" + out.RawString(prefix) + (in.Struct).MarshalEasyJSON(out) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v TestingStruct) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson84e285b8EncodeGithubComARMSoftwareGolangUtilsUtilsSerializationJsonJsontestEasyjson(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v TestingStruct) MarshalEasyJSON(w *jwriter.Writer) { + easyjson84e285b8EncodeGithubComARMSoftwareGolangUtilsUtilsSerializationJsonJsontestEasyjson(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *TestingStruct) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson84e285b8DecodeGithubComARMSoftwareGolangUtilsUtilsSerializationJsonJsontestEasyjson(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *TestingStruct) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson84e285b8DecodeGithubComARMSoftwareGolangUtilsUtilsSerializationJsonJsontestEasyjson(l, v) +} +func easyjson84e285b8DecodeGithubComARMSoftwareGolangUtilsUtilsSerializationJsonJsontestEasyjson1(in *jlexer.Lexer, out *BStruct) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "Image": + out.Image = string(in.String()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson84e285b8EncodeGithubComARMSoftwareGolangUtilsUtilsSerializationJsonJsontestEasyjson1(out *jwriter.Writer, in BStruct) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"Image\":" + out.RawString(prefix[1:]) + out.String(string(in.Image)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BStruct) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson84e285b8EncodeGithubComARMSoftwareGolangUtilsUtilsSerializationJsonJsontestEasyjson1(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BStruct) MarshalEasyJSON(w *jwriter.Writer) { + easyjson84e285b8EncodeGithubComARMSoftwareGolangUtilsUtilsSerializationJsonJsontestEasyjson1(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BStruct) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson84e285b8DecodeGithubComARMSoftwareGolangUtilsUtilsSerializationJsonJsontestEasyjson1(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BStruct) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson84e285b8DecodeGithubComARMSoftwareGolangUtilsUtilsSerializationJsonJsontestEasyjson1(l, v) +} +func easyjson84e285b8DecodeGithubComARMSoftwareGolangUtilsUtilsSerializationJsonJsontestEasyjson2(in *jlexer.Lexer, out *AStruct) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "Number": + out.Number = int64(in.Int64()) + case "Height": + out.Height = int64(in.Int64()) + case "AnotherStruct": + (out.AnotherStruct).UnmarshalEasyJSON(in) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson84e285b8EncodeGithubComARMSoftwareGolangUtilsUtilsSerializationJsonJsontestEasyjson2(out *jwriter.Writer, in AStruct) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"Number\":" + out.RawString(prefix[1:]) + out.Int64(int64(in.Number)) + } + { + const prefix string = ",\"Height\":" + out.RawString(prefix) + out.Int64(int64(in.Height)) + } + { + const prefix string = ",\"AnotherStruct\":" + out.RawString(prefix) + (in.AnotherStruct).MarshalEasyJSON(out) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v AStruct) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson84e285b8EncodeGithubComARMSoftwareGolangUtilsUtilsSerializationJsonJsontestEasyjson2(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v AStruct) MarshalEasyJSON(w *jwriter.Writer) { + easyjson84e285b8EncodeGithubComARMSoftwareGolangUtilsUtilsSerializationJsonJsontestEasyjson2(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *AStruct) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson84e285b8DecodeGithubComARMSoftwareGolangUtilsUtilsSerializationJsonJsontestEasyjson2(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *AStruct) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson84e285b8DecodeGithubComARMSoftwareGolangUtilsUtilsSerializationJsonJsontestEasyjson2(l, v) +} diff --git a/utils/serialization/json/jsontest/ffjson/testing_types.go b/utils/serialization/json/jsontest/ffjson/testing_types.go new file mode 100644 index 0000000000..d7bceb2ab9 --- /dev/null +++ b/utils/serialization/json/jsontest/ffjson/testing_types.go @@ -0,0 +1,138 @@ +package ffjson + +import ( + "slices" + "time" + + "github.com/ARM-software/golang-utils/utils/collection" +) + +//go:generate go tool ffjson $GOFILE + +type TestingStruct struct { + Int int + Int8 int8 + Int16 int16 + Int32 int32 + Int64 int64 + String string + Bool bool + Duration time.Duration + SString []string + SInt []int + SInt8 []int8 + SInt16 []int16 + SInt32 []int32 + SInt64 []int64 + SFloat32 []float32 + SFloat64 []float64 + SBool []bool + Struct AStruct +} + +func (s *TestingStruct) Equals(other any) bool { + if s == other { + return true + } + + if other == nil { + return s == nil + } + + if o, ok := other.(*TestingStruct); ok { + return collection.All([]bool{ + s.Int == o.Int, + s.Int8 == o.Int8, + s.Int16 == o.Int16, + s.Int32 == o.Int32, + s.Int64 == o.Int64, + s.String == o.String, + s.Bool == o.Bool, + s.Duration == o.Duration, + slices.Equal(s.SString, o.SString), + slices.Equal(s.SInt, o.SInt), + slices.Equal(s.SInt8, o.SInt8), + slices.Equal(s.SInt16, o.SInt16), + slices.Equal(s.SInt32, o.SInt32), + slices.Equal(s.SInt64, o.SInt64), + slices.Equal(s.SFloat32, o.SFloat32), + slices.Equal(s.SFloat64, o.SFloat64), + slices.Equal(s.SBool, o.SBool), + s.Struct.Equals(o.Struct), + }) + } + + if o, ok := other.(TestingStruct); ok { + return collection.All([]bool{ + s.Int == o.Int, + s.Int8 == o.Int8, + s.Int16 == o.Int16, + s.Int32 == o.Int32, + s.Int64 == o.Int64, + s.String == o.String, + s.Bool == o.Bool, + slices.Equal(s.SString, o.SString), + slices.Equal(s.SInt, o.SInt), + slices.Equal(s.SInt8, o.SInt8), + slices.Equal(s.SInt16, o.SInt16), + slices.Equal(s.SInt32, o.SInt32), + slices.Equal(s.SInt64, o.SInt64), + slices.Equal(s.SFloat32, o.SFloat32), + slices.Equal(s.SFloat64, o.SFloat64), + slices.Equal(s.SBool, o.SBool), + s.Struct.Equals(o.Struct), + }) + } + + return false +} + +type AStruct struct { + Number int64 + Height int64 + AnotherStruct BStruct +} + +func (s *AStruct) Equals(other any) bool { + if s == other { + return true + } + + if other == nil { + return s == nil + } + + if o, ok := other.(*AStruct); ok { + return s.Number == o.Number && s.Height == o.Height && s.AnotherStruct.Equals(&o.AnotherStruct) + } + + if o, ok := other.(AStruct); ok { + return s.Number == o.Number && s.Height == o.Height && s.AnotherStruct.Equals(&o.AnotherStruct) + } + + return false +} + +type BStruct struct { + Image string +} + +func (s *BStruct) Equals(other any) bool { + if s == other { + return true + } + + if other == nil { + return s == nil + } + + if o, ok := other.(*BStruct); ok { + return s.Image == o.Image + } + + if o, ok := other.(BStruct); ok { + return s.Image == o.Image + } + + return false +} diff --git a/utils/serialization/json/jsontest/ffjson/testing_types_ffjson.go b/utils/serialization/json/jsontest/ffjson/testing_types_ffjson.go new file mode 100644 index 0000000000..7371de69ab --- /dev/null +++ b/utils/serialization/json/jsontest/ffjson/testing_types_ffjson.go @@ -0,0 +1,2103 @@ +// Code generated by ffjson . DO NOT EDIT. +// source: testing_types.go + +package ffjson + +import ( + "bytes" + "errors" + "fmt" + fflib "github.com/pquerna/ffjson/fflib/v1" + "time" +) + +// MarshalJSON marshal bytes to json - template +func (j *AStruct) MarshalJSON() ([]byte, error) { + var buf fflib.Buffer + if j == nil { + buf.WriteString("null") + return buf.Bytes(), nil + } + err := j.MarshalJSONBuf(&buf) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// MarshalJSONBuf marshal buff to json - template +func (j *AStruct) MarshalJSONBuf(buf fflib.EncodingBuffer) error { + if j == nil { + buf.WriteString("null") + return nil + } + var err error + var obj []byte + _ = obj + _ = err + buf.WriteString(`{"Number":`) + fflib.FormatBits2(buf, uint64(j.Number), 10, j.Number < 0) + buf.WriteString(`,"Height":`) + fflib.FormatBits2(buf, uint64(j.Height), 10, j.Height < 0) + buf.WriteString(`,"AnotherStruct":`) + + { + + err = j.AnotherStruct.MarshalJSONBuf(buf) + if err != nil { + return err + } + + } + buf.WriteByte('}') + return nil +} + +const ( + ffjtAStructbase = iota + ffjtAStructnosuchkey + + ffjtAStructNumber + + ffjtAStructHeight + + ffjtAStructAnotherStruct +) + +var ffjKeyAStructNumber = []byte("Number") + +var ffjKeyAStructHeight = []byte("Height") + +var ffjKeyAStructAnotherStruct = []byte("AnotherStruct") + +// UnmarshalJSON umarshall json - template of ffjson +func (j *AStruct) UnmarshalJSON(input []byte) error { + fs := fflib.NewFFLexer(input) + return j.UnmarshalJSONFFLexer(fs, fflib.FFParse_map_start) +} + +// UnmarshalJSONFFLexer fast json unmarshall - template ffjson +func (j *AStruct) UnmarshalJSONFFLexer(fs *fflib.FFLexer, state fflib.FFParseState) error { + var err error + currentKey := ffjtAStructbase + _ = currentKey + tok := fflib.FFTok_init + wantedTok := fflib.FFTok_init + +mainparse: + for { + tok = fs.Scan() + // println(fmt.Sprintf("debug: tok: %v state: %v", tok, state)) + if tok == fflib.FFTok_error { + goto tokerror + } + + switch state { + + case fflib.FFParse_map_start: + if tok != fflib.FFTok_left_bracket { + wantedTok = fflib.FFTok_left_bracket + goto wrongtokenerror + } + state = fflib.FFParse_want_key + continue + + case fflib.FFParse_after_value: + if tok == fflib.FFTok_comma { + state = fflib.FFParse_want_key + } else if tok == fflib.FFTok_right_bracket { + goto done + } else { + wantedTok = fflib.FFTok_comma + goto wrongtokenerror + } + + case fflib.FFParse_want_key: + // json {} ended. goto exit. woo. + if tok == fflib.FFTok_right_bracket { + goto done + } + if tok != fflib.FFTok_string { + wantedTok = fflib.FFTok_string + goto wrongtokenerror + } + + kn := fs.Output.Bytes() + if len(kn) <= 0 { + // "" case. hrm. + currentKey = ffjtAStructnosuchkey + state = fflib.FFParse_want_colon + goto mainparse + } else { + switch kn[0] { + + case 'A': + + if bytes.Equal(ffjKeyAStructAnotherStruct, kn) { + currentKey = ffjtAStructAnotherStruct + state = fflib.FFParse_want_colon + goto mainparse + } + + case 'H': + + if bytes.Equal(ffjKeyAStructHeight, kn) { + currentKey = ffjtAStructHeight + state = fflib.FFParse_want_colon + goto mainparse + } + + case 'N': + + if bytes.Equal(ffjKeyAStructNumber, kn) { + currentKey = ffjtAStructNumber + state = fflib.FFParse_want_colon + goto mainparse + } + + } + + if fflib.EqualFoldRight(ffjKeyAStructAnotherStruct, kn) { + currentKey = ffjtAStructAnotherStruct + state = fflib.FFParse_want_colon + goto mainparse + } + + if fflib.SimpleLetterEqualFold(ffjKeyAStructHeight, kn) { + currentKey = ffjtAStructHeight + state = fflib.FFParse_want_colon + goto mainparse + } + + if fflib.SimpleLetterEqualFold(ffjKeyAStructNumber, kn) { + currentKey = ffjtAStructNumber + state = fflib.FFParse_want_colon + goto mainparse + } + + currentKey = ffjtAStructnosuchkey + state = fflib.FFParse_want_colon + goto mainparse + } + + case fflib.FFParse_want_colon: + if tok != fflib.FFTok_colon { + wantedTok = fflib.FFTok_colon + goto wrongtokenerror + } + state = fflib.FFParse_want_value + continue + case fflib.FFParse_want_value: + + if tok == fflib.FFTok_left_brace || tok == fflib.FFTok_left_bracket || tok == fflib.FFTok_integer || tok == fflib.FFTok_double || tok == fflib.FFTok_string || tok == fflib.FFTok_bool || tok == fflib.FFTok_null { + switch currentKey { + + case ffjtAStructNumber: + goto handle_Number + + case ffjtAStructHeight: + goto handle_Height + + case ffjtAStructAnotherStruct: + goto handle_AnotherStruct + + case ffjtAStructnosuchkey: + err = fs.SkipField(tok) + if err != nil { + return fs.WrapErr(err) + } + state = fflib.FFParse_after_value + goto mainparse + } + } else { + goto wantedvalue + } + } + } + +handle_Number: + + /* handler: j.Number type=int64 kind=int64 quoted=false*/ + + { + if tok != fflib.FFTok_integer && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for int64", tok)) + } + } + + { + + if tok == fflib.FFTok_null { + + } else { + + tval, err := fflib.ParseInt(fs.Output.Bytes(), 10, 64) + + if err != nil { + return fs.WrapErr(err) + } + + j.Number = int64(tval) + + } + } + + state = fflib.FFParse_after_value + goto mainparse + +handle_Height: + + /* handler: j.Height type=int64 kind=int64 quoted=false*/ + + { + if tok != fflib.FFTok_integer && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for int64", tok)) + } + } + + { + + if tok == fflib.FFTok_null { + + } else { + + tval, err := fflib.ParseInt(fs.Output.Bytes(), 10, 64) + + if err != nil { + return fs.WrapErr(err) + } + + j.Height = int64(tval) + + } + } + + state = fflib.FFParse_after_value + goto mainparse + +handle_AnotherStruct: + + /* handler: j.AnotherStruct type=ffjson.BStruct kind=struct quoted=false*/ + + { + if tok == fflib.FFTok_null { + + } else { + + err = j.AnotherStruct.UnmarshalJSONFFLexer(fs, fflib.FFParse_want_key) + if err != nil { + return err + } + } + state = fflib.FFParse_after_value + } + + state = fflib.FFParse_after_value + goto mainparse + +wantedvalue: + return fs.WrapErr(fmt.Errorf("wanted value token, but got token: %v", tok)) +wrongtokenerror: + return fs.WrapErr(fmt.Errorf("ffjson: wanted token: %v, but got token: %v output=%s", wantedTok, tok, fs.Output.String())) +tokerror: + if fs.BigError != nil { + return fs.WrapErr(fs.BigError) + } + err = fs.Error.ToError() + if err != nil { + return fs.WrapErr(err) + } + panic("ffjson-generated: unreachable, please report bug.") +done: + + return nil +} + +// MarshalJSON marshal bytes to json - template +func (j *BStruct) MarshalJSON() ([]byte, error) { + var buf fflib.Buffer + if j == nil { + buf.WriteString("null") + return buf.Bytes(), nil + } + err := j.MarshalJSONBuf(&buf) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// MarshalJSONBuf marshal buff to json - template +func (j *BStruct) MarshalJSONBuf(buf fflib.EncodingBuffer) error { + if j == nil { + buf.WriteString("null") + return nil + } + var err error + var obj []byte + _ = obj + _ = err + buf.WriteString(`{"Image":`) + fflib.WriteJsonString(buf, string(j.Image)) + buf.WriteByte('}') + return nil +} + +const ( + ffjtBStructbase = iota + ffjtBStructnosuchkey + + ffjtBStructImage +) + +var ffjKeyBStructImage = []byte("Image") + +// UnmarshalJSON umarshall json - template of ffjson +func (j *BStruct) UnmarshalJSON(input []byte) error { + fs := fflib.NewFFLexer(input) + return j.UnmarshalJSONFFLexer(fs, fflib.FFParse_map_start) +} + +// UnmarshalJSONFFLexer fast json unmarshall - template ffjson +func (j *BStruct) UnmarshalJSONFFLexer(fs *fflib.FFLexer, state fflib.FFParseState) error { + var err error + currentKey := ffjtBStructbase + _ = currentKey + tok := fflib.FFTok_init + wantedTok := fflib.FFTok_init + +mainparse: + for { + tok = fs.Scan() + // println(fmt.Sprintf("debug: tok: %v state: %v", tok, state)) + if tok == fflib.FFTok_error { + goto tokerror + } + + switch state { + + case fflib.FFParse_map_start: + if tok != fflib.FFTok_left_bracket { + wantedTok = fflib.FFTok_left_bracket + goto wrongtokenerror + } + state = fflib.FFParse_want_key + continue + + case fflib.FFParse_after_value: + if tok == fflib.FFTok_comma { + state = fflib.FFParse_want_key + } else if tok == fflib.FFTok_right_bracket { + goto done + } else { + wantedTok = fflib.FFTok_comma + goto wrongtokenerror + } + + case fflib.FFParse_want_key: + // json {} ended. goto exit. woo. + if tok == fflib.FFTok_right_bracket { + goto done + } + if tok != fflib.FFTok_string { + wantedTok = fflib.FFTok_string + goto wrongtokenerror + } + + kn := fs.Output.Bytes() + if len(kn) <= 0 { + // "" case. hrm. + currentKey = ffjtBStructnosuchkey + state = fflib.FFParse_want_colon + goto mainparse + } else { + switch kn[0] { + + case 'I': + + if bytes.Equal(ffjKeyBStructImage, kn) { + currentKey = ffjtBStructImage + state = fflib.FFParse_want_colon + goto mainparse + } + + } + + if fflib.SimpleLetterEqualFold(ffjKeyBStructImage, kn) { + currentKey = ffjtBStructImage + state = fflib.FFParse_want_colon + goto mainparse + } + + currentKey = ffjtBStructnosuchkey + state = fflib.FFParse_want_colon + goto mainparse + } + + case fflib.FFParse_want_colon: + if tok != fflib.FFTok_colon { + wantedTok = fflib.FFTok_colon + goto wrongtokenerror + } + state = fflib.FFParse_want_value + continue + case fflib.FFParse_want_value: + + if tok == fflib.FFTok_left_brace || tok == fflib.FFTok_left_bracket || tok == fflib.FFTok_integer || tok == fflib.FFTok_double || tok == fflib.FFTok_string || tok == fflib.FFTok_bool || tok == fflib.FFTok_null { + switch currentKey { + + case ffjtBStructImage: + goto handle_Image + + case ffjtBStructnosuchkey: + err = fs.SkipField(tok) + if err != nil { + return fs.WrapErr(err) + } + state = fflib.FFParse_after_value + goto mainparse + } + } else { + goto wantedvalue + } + } + } + +handle_Image: + + /* handler: j.Image type=string kind=string quoted=false*/ + + { + + { + if tok != fflib.FFTok_string && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for string", tok)) + } + } + + if tok == fflib.FFTok_null { + + } else { + + outBuf := fs.Output.Bytes() + + j.Image = string(string(outBuf)) + + } + } + + state = fflib.FFParse_after_value + goto mainparse + +wantedvalue: + return fs.WrapErr(fmt.Errorf("wanted value token, but got token: %v", tok)) +wrongtokenerror: + return fs.WrapErr(fmt.Errorf("ffjson: wanted token: %v, but got token: %v output=%s", wantedTok, tok, fs.Output.String())) +tokerror: + if fs.BigError != nil { + return fs.WrapErr(fs.BigError) + } + err = fs.Error.ToError() + if err != nil { + return fs.WrapErr(err) + } + panic("ffjson-generated: unreachable, please report bug.") +done: + + return nil +} + +// MarshalJSON marshal bytes to json - template +func (j *TestingStruct) MarshalJSON() ([]byte, error) { + var buf fflib.Buffer + if j == nil { + buf.WriteString("null") + return buf.Bytes(), nil + } + err := j.MarshalJSONBuf(&buf) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// MarshalJSONBuf marshal buff to json - template +func (j *TestingStruct) MarshalJSONBuf(buf fflib.EncodingBuffer) error { + if j == nil { + buf.WriteString("null") + return nil + } + var err error + var obj []byte + _ = obj + _ = err + buf.WriteString(`{"Int":`) + fflib.FormatBits2(buf, uint64(j.Int), 10, j.Int < 0) + buf.WriteString(`,"Int8":`) + fflib.FormatBits2(buf, uint64(j.Int8), 10, j.Int8 < 0) + buf.WriteString(`,"Int16":`) + fflib.FormatBits2(buf, uint64(j.Int16), 10, j.Int16 < 0) + buf.WriteString(`,"Int32":`) + fflib.FormatBits2(buf, uint64(j.Int32), 10, j.Int32 < 0) + buf.WriteString(`,"Int64":`) + fflib.FormatBits2(buf, uint64(j.Int64), 10, j.Int64 < 0) + buf.WriteString(`,"String":`) + fflib.WriteJsonString(buf, string(j.String)) + if j.Bool { + buf.WriteString(`,"Bool":true`) + } else { + buf.WriteString(`,"Bool":false`) + } + buf.WriteString(`,"Duration":`) + fflib.FormatBits2(buf, uint64(j.Duration), 10, j.Duration < 0) + buf.WriteString(`,"SString":`) + if j.SString != nil { + buf.WriteString(`[`) + for i, v := range j.SString { + if i != 0 { + buf.WriteString(`,`) + } + fflib.WriteJsonString(buf, string(v)) + } + buf.WriteString(`]`) + } else { + buf.WriteString(`null`) + } + buf.WriteString(`,"SInt":`) + if j.SInt != nil { + buf.WriteString(`[`) + for i, v := range j.SInt { + if i != 0 { + buf.WriteString(`,`) + } + fflib.FormatBits2(buf, uint64(v), 10, v < 0) + } + buf.WriteString(`]`) + } else { + buf.WriteString(`null`) + } + buf.WriteString(`,"SInt8":`) + if j.SInt8 != nil { + buf.WriteString(`[`) + for i, v := range j.SInt8 { + if i != 0 { + buf.WriteString(`,`) + } + fflib.FormatBits2(buf, uint64(v), 10, v < 0) + } + buf.WriteString(`]`) + } else { + buf.WriteString(`null`) + } + buf.WriteString(`,"SInt16":`) + if j.SInt16 != nil { + buf.WriteString(`[`) + for i, v := range j.SInt16 { + if i != 0 { + buf.WriteString(`,`) + } + fflib.FormatBits2(buf, uint64(v), 10, v < 0) + } + buf.WriteString(`]`) + } else { + buf.WriteString(`null`) + } + buf.WriteString(`,"SInt32":`) + if j.SInt32 != nil { + buf.WriteString(`[`) + for i, v := range j.SInt32 { + if i != 0 { + buf.WriteString(`,`) + } + fflib.FormatBits2(buf, uint64(v), 10, v < 0) + } + buf.WriteString(`]`) + } else { + buf.WriteString(`null`) + } + buf.WriteString(`,"SInt64":`) + if j.SInt64 != nil { + buf.WriteString(`[`) + for i, v := range j.SInt64 { + if i != 0 { + buf.WriteString(`,`) + } + fflib.FormatBits2(buf, uint64(v), 10, v < 0) + } + buf.WriteString(`]`) + } else { + buf.WriteString(`null`) + } + buf.WriteString(`,"SFloat32":`) + if j.SFloat32 != nil { + buf.WriteString(`[`) + for i, v := range j.SFloat32 { + if i != 0 { + buf.WriteString(`,`) + } + fflib.AppendFloat(buf, float64(v), 'g', -1, 32) + } + buf.WriteString(`]`) + } else { + buf.WriteString(`null`) + } + buf.WriteString(`,"SFloat64":`) + if j.SFloat64 != nil { + buf.WriteString(`[`) + for i, v := range j.SFloat64 { + if i != 0 { + buf.WriteString(`,`) + } + fflib.AppendFloat(buf, float64(v), 'g', -1, 64) + } + buf.WriteString(`]`) + } else { + buf.WriteString(`null`) + } + buf.WriteString(`,"SBool":`) + if j.SBool != nil { + buf.WriteString(`[`) + for i, v := range j.SBool { + if i != 0 { + buf.WriteString(`,`) + } + if v { + buf.WriteString(`true`) + } else { + buf.WriteString(`false`) + } + } + buf.WriteString(`]`) + } else { + buf.WriteString(`null`) + } + buf.WriteString(`,"Struct":`) + + { + + err = j.Struct.MarshalJSONBuf(buf) + if err != nil { + return err + } + + } + buf.WriteByte('}') + return nil +} + +const ( + ffjtTestingStructbase = iota + ffjtTestingStructnosuchkey + + ffjtTestingStructInt + + ffjtTestingStructInt8 + + ffjtTestingStructInt16 + + ffjtTestingStructInt32 + + ffjtTestingStructInt64 + + ffjtTestingStructString + + ffjtTestingStructBool + + ffjtTestingStructDuration + + ffjtTestingStructSString + + ffjtTestingStructSInt + + ffjtTestingStructSInt8 + + ffjtTestingStructSInt16 + + ffjtTestingStructSInt32 + + ffjtTestingStructSInt64 + + ffjtTestingStructSFloat32 + + ffjtTestingStructSFloat64 + + ffjtTestingStructSBool + + ffjtTestingStructStruct +) + +var ffjKeyTestingStructInt = []byte("Int") + +var ffjKeyTestingStructInt8 = []byte("Int8") + +var ffjKeyTestingStructInt16 = []byte("Int16") + +var ffjKeyTestingStructInt32 = []byte("Int32") + +var ffjKeyTestingStructInt64 = []byte("Int64") + +var ffjKeyTestingStructString = []byte("String") + +var ffjKeyTestingStructBool = []byte("Bool") + +var ffjKeyTestingStructDuration = []byte("Duration") + +var ffjKeyTestingStructSString = []byte("SString") + +var ffjKeyTestingStructSInt = []byte("SInt") + +var ffjKeyTestingStructSInt8 = []byte("SInt8") + +var ffjKeyTestingStructSInt16 = []byte("SInt16") + +var ffjKeyTestingStructSInt32 = []byte("SInt32") + +var ffjKeyTestingStructSInt64 = []byte("SInt64") + +var ffjKeyTestingStructSFloat32 = []byte("SFloat32") + +var ffjKeyTestingStructSFloat64 = []byte("SFloat64") + +var ffjKeyTestingStructSBool = []byte("SBool") + +var ffjKeyTestingStructStruct = []byte("Struct") + +// UnmarshalJSON umarshall json - template of ffjson +func (j *TestingStruct) UnmarshalJSON(input []byte) error { + fs := fflib.NewFFLexer(input) + return j.UnmarshalJSONFFLexer(fs, fflib.FFParse_map_start) +} + +// UnmarshalJSONFFLexer fast json unmarshall - template ffjson +func (j *TestingStruct) UnmarshalJSONFFLexer(fs *fflib.FFLexer, state fflib.FFParseState) error { + var err error + currentKey := ffjtTestingStructbase + _ = currentKey + tok := fflib.FFTok_init + wantedTok := fflib.FFTok_init + +mainparse: + for { + tok = fs.Scan() + // println(fmt.Sprintf("debug: tok: %v state: %v", tok, state)) + if tok == fflib.FFTok_error { + goto tokerror + } + + switch state { + + case fflib.FFParse_map_start: + if tok != fflib.FFTok_left_bracket { + wantedTok = fflib.FFTok_left_bracket + goto wrongtokenerror + } + state = fflib.FFParse_want_key + continue + + case fflib.FFParse_after_value: + if tok == fflib.FFTok_comma { + state = fflib.FFParse_want_key + } else if tok == fflib.FFTok_right_bracket { + goto done + } else { + wantedTok = fflib.FFTok_comma + goto wrongtokenerror + } + + case fflib.FFParse_want_key: + // json {} ended. goto exit. woo. + if tok == fflib.FFTok_right_bracket { + goto done + } + if tok != fflib.FFTok_string { + wantedTok = fflib.FFTok_string + goto wrongtokenerror + } + + kn := fs.Output.Bytes() + if len(kn) <= 0 { + // "" case. hrm. + currentKey = ffjtTestingStructnosuchkey + state = fflib.FFParse_want_colon + goto mainparse + } else { + switch kn[0] { + + case 'B': + + if bytes.Equal(ffjKeyTestingStructBool, kn) { + currentKey = ffjtTestingStructBool + state = fflib.FFParse_want_colon + goto mainparse + } + + case 'D': + + if bytes.Equal(ffjKeyTestingStructDuration, kn) { + currentKey = ffjtTestingStructDuration + state = fflib.FFParse_want_colon + goto mainparse + } + + case 'I': + + if bytes.Equal(ffjKeyTestingStructInt, kn) { + currentKey = ffjtTestingStructInt + state = fflib.FFParse_want_colon + goto mainparse + + } else if bytes.Equal(ffjKeyTestingStructInt8, kn) { + currentKey = ffjtTestingStructInt8 + state = fflib.FFParse_want_colon + goto mainparse + + } else if bytes.Equal(ffjKeyTestingStructInt16, kn) { + currentKey = ffjtTestingStructInt16 + state = fflib.FFParse_want_colon + goto mainparse + + } else if bytes.Equal(ffjKeyTestingStructInt32, kn) { + currentKey = ffjtTestingStructInt32 + state = fflib.FFParse_want_colon + goto mainparse + + } else if bytes.Equal(ffjKeyTestingStructInt64, kn) { + currentKey = ffjtTestingStructInt64 + state = fflib.FFParse_want_colon + goto mainparse + } + + case 'S': + + if bytes.Equal(ffjKeyTestingStructString, kn) { + currentKey = ffjtTestingStructString + state = fflib.FFParse_want_colon + goto mainparse + + } else if bytes.Equal(ffjKeyTestingStructSString, kn) { + currentKey = ffjtTestingStructSString + state = fflib.FFParse_want_colon + goto mainparse + + } else if bytes.Equal(ffjKeyTestingStructSInt, kn) { + currentKey = ffjtTestingStructSInt + state = fflib.FFParse_want_colon + goto mainparse + + } else if bytes.Equal(ffjKeyTestingStructSInt8, kn) { + currentKey = ffjtTestingStructSInt8 + state = fflib.FFParse_want_colon + goto mainparse + + } else if bytes.Equal(ffjKeyTestingStructSInt16, kn) { + currentKey = ffjtTestingStructSInt16 + state = fflib.FFParse_want_colon + goto mainparse + + } else if bytes.Equal(ffjKeyTestingStructSInt32, kn) { + currentKey = ffjtTestingStructSInt32 + state = fflib.FFParse_want_colon + goto mainparse + + } else if bytes.Equal(ffjKeyTestingStructSInt64, kn) { + currentKey = ffjtTestingStructSInt64 + state = fflib.FFParse_want_colon + goto mainparse + + } else if bytes.Equal(ffjKeyTestingStructSFloat32, kn) { + currentKey = ffjtTestingStructSFloat32 + state = fflib.FFParse_want_colon + goto mainparse + + } else if bytes.Equal(ffjKeyTestingStructSFloat64, kn) { + currentKey = ffjtTestingStructSFloat64 + state = fflib.FFParse_want_colon + goto mainparse + + } else if bytes.Equal(ffjKeyTestingStructSBool, kn) { + currentKey = ffjtTestingStructSBool + state = fflib.FFParse_want_colon + goto mainparse + + } else if bytes.Equal(ffjKeyTestingStructStruct, kn) { + currentKey = ffjtTestingStructStruct + state = fflib.FFParse_want_colon + goto mainparse + } + + } + + if fflib.EqualFoldRight(ffjKeyTestingStructStruct, kn) { + currentKey = ffjtTestingStructStruct + state = fflib.FFParse_want_colon + goto mainparse + } + + if fflib.EqualFoldRight(ffjKeyTestingStructSBool, kn) { + currentKey = ffjtTestingStructSBool + state = fflib.FFParse_want_colon + goto mainparse + } + + if fflib.EqualFoldRight(ffjKeyTestingStructSFloat64, kn) { + currentKey = ffjtTestingStructSFloat64 + state = fflib.FFParse_want_colon + goto mainparse + } + + if fflib.EqualFoldRight(ffjKeyTestingStructSFloat32, kn) { + currentKey = ffjtTestingStructSFloat32 + state = fflib.FFParse_want_colon + goto mainparse + } + + if fflib.EqualFoldRight(ffjKeyTestingStructSInt64, kn) { + currentKey = ffjtTestingStructSInt64 + state = fflib.FFParse_want_colon + goto mainparse + } + + if fflib.EqualFoldRight(ffjKeyTestingStructSInt32, kn) { + currentKey = ffjtTestingStructSInt32 + state = fflib.FFParse_want_colon + goto mainparse + } + + if fflib.EqualFoldRight(ffjKeyTestingStructSInt16, kn) { + currentKey = ffjtTestingStructSInt16 + state = fflib.FFParse_want_colon + goto mainparse + } + + if fflib.EqualFoldRight(ffjKeyTestingStructSInt8, kn) { + currentKey = ffjtTestingStructSInt8 + state = fflib.FFParse_want_colon + goto mainparse + } + + if fflib.EqualFoldRight(ffjKeyTestingStructSInt, kn) { + currentKey = ffjtTestingStructSInt + state = fflib.FFParse_want_colon + goto mainparse + } + + if fflib.EqualFoldRight(ffjKeyTestingStructSString, kn) { + currentKey = ffjtTestingStructSString + state = fflib.FFParse_want_colon + goto mainparse + } + + if fflib.SimpleLetterEqualFold(ffjKeyTestingStructDuration, kn) { + currentKey = ffjtTestingStructDuration + state = fflib.FFParse_want_colon + goto mainparse + } + + if fflib.SimpleLetterEqualFold(ffjKeyTestingStructBool, kn) { + currentKey = ffjtTestingStructBool + state = fflib.FFParse_want_colon + goto mainparse + } + + if fflib.EqualFoldRight(ffjKeyTestingStructString, kn) { + currentKey = ffjtTestingStructString + state = fflib.FFParse_want_colon + goto mainparse + } + + if fflib.AsciiEqualFold(ffjKeyTestingStructInt64, kn) { + currentKey = ffjtTestingStructInt64 + state = fflib.FFParse_want_colon + goto mainparse + } + + if fflib.AsciiEqualFold(ffjKeyTestingStructInt32, kn) { + currentKey = ffjtTestingStructInt32 + state = fflib.FFParse_want_colon + goto mainparse + } + + if fflib.AsciiEqualFold(ffjKeyTestingStructInt16, kn) { + currentKey = ffjtTestingStructInt16 + state = fflib.FFParse_want_colon + goto mainparse + } + + if fflib.AsciiEqualFold(ffjKeyTestingStructInt8, kn) { + currentKey = ffjtTestingStructInt8 + state = fflib.FFParse_want_colon + goto mainparse + } + + if fflib.SimpleLetterEqualFold(ffjKeyTestingStructInt, kn) { + currentKey = ffjtTestingStructInt + state = fflib.FFParse_want_colon + goto mainparse + } + + currentKey = ffjtTestingStructnosuchkey + state = fflib.FFParse_want_colon + goto mainparse + } + + case fflib.FFParse_want_colon: + if tok != fflib.FFTok_colon { + wantedTok = fflib.FFTok_colon + goto wrongtokenerror + } + state = fflib.FFParse_want_value + continue + case fflib.FFParse_want_value: + + if tok == fflib.FFTok_left_brace || tok == fflib.FFTok_left_bracket || tok == fflib.FFTok_integer || tok == fflib.FFTok_double || tok == fflib.FFTok_string || tok == fflib.FFTok_bool || tok == fflib.FFTok_null { + switch currentKey { + + case ffjtTestingStructInt: + goto handle_Int + + case ffjtTestingStructInt8: + goto handle_Int8 + + case ffjtTestingStructInt16: + goto handle_Int16 + + case ffjtTestingStructInt32: + goto handle_Int32 + + case ffjtTestingStructInt64: + goto handle_Int64 + + case ffjtTestingStructString: + goto handle_String + + case ffjtTestingStructBool: + goto handle_Bool + + case ffjtTestingStructDuration: + goto handle_Duration + + case ffjtTestingStructSString: + goto handle_SString + + case ffjtTestingStructSInt: + goto handle_SInt + + case ffjtTestingStructSInt8: + goto handle_SInt8 + + case ffjtTestingStructSInt16: + goto handle_SInt16 + + case ffjtTestingStructSInt32: + goto handle_SInt32 + + case ffjtTestingStructSInt64: + goto handle_SInt64 + + case ffjtTestingStructSFloat32: + goto handle_SFloat32 + + case ffjtTestingStructSFloat64: + goto handle_SFloat64 + + case ffjtTestingStructSBool: + goto handle_SBool + + case ffjtTestingStructStruct: + goto handle_Struct + + case ffjtTestingStructnosuchkey: + err = fs.SkipField(tok) + if err != nil { + return fs.WrapErr(err) + } + state = fflib.FFParse_after_value + goto mainparse + } + } else { + goto wantedvalue + } + } + } + +handle_Int: + + /* handler: j.Int type=int kind=int quoted=false*/ + + { + if tok != fflib.FFTok_integer && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for int", tok)) + } + } + + { + + if tok == fflib.FFTok_null { + + } else { + + tval, err := fflib.ParseInt(fs.Output.Bytes(), 10, 64) + + if err != nil { + return fs.WrapErr(err) + } + + j.Int = int(tval) + + } + } + + state = fflib.FFParse_after_value + goto mainparse + +handle_Int8: + + /* handler: j.Int8 type=int8 kind=int8 quoted=false*/ + + { + if tok != fflib.FFTok_integer && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for int8", tok)) + } + } + + { + + if tok == fflib.FFTok_null { + + } else { + + tval, err := fflib.ParseInt(fs.Output.Bytes(), 10, 8) + + if err != nil { + return fs.WrapErr(err) + } + + j.Int8 = int8(tval) + + } + } + + state = fflib.FFParse_after_value + goto mainparse + +handle_Int16: + + /* handler: j.Int16 type=int16 kind=int16 quoted=false*/ + + { + if tok != fflib.FFTok_integer && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for int16", tok)) + } + } + + { + + if tok == fflib.FFTok_null { + + } else { + + tval, err := fflib.ParseInt(fs.Output.Bytes(), 10, 16) + + if err != nil { + return fs.WrapErr(err) + } + + j.Int16 = int16(tval) + + } + } + + state = fflib.FFParse_after_value + goto mainparse + +handle_Int32: + + /* handler: j.Int32 type=int32 kind=int32 quoted=false*/ + + { + if tok != fflib.FFTok_integer && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for int32", tok)) + } + } + + { + + if tok == fflib.FFTok_null { + + } else { + + tval, err := fflib.ParseInt(fs.Output.Bytes(), 10, 32) + + if err != nil { + return fs.WrapErr(err) + } + + j.Int32 = int32(tval) + + } + } + + state = fflib.FFParse_after_value + goto mainparse + +handle_Int64: + + /* handler: j.Int64 type=int64 kind=int64 quoted=false*/ + + { + if tok != fflib.FFTok_integer && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for int64", tok)) + } + } + + { + + if tok == fflib.FFTok_null { + + } else { + + tval, err := fflib.ParseInt(fs.Output.Bytes(), 10, 64) + + if err != nil { + return fs.WrapErr(err) + } + + j.Int64 = int64(tval) + + } + } + + state = fflib.FFParse_after_value + goto mainparse + +handle_String: + + /* handler: j.String type=string kind=string quoted=false*/ + + { + + { + if tok != fflib.FFTok_string && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for string", tok)) + } + } + + if tok == fflib.FFTok_null { + + } else { + + outBuf := fs.Output.Bytes() + + j.String = string(string(outBuf)) + + } + } + + state = fflib.FFParse_after_value + goto mainparse + +handle_Bool: + + /* handler: j.Bool type=bool kind=bool quoted=false*/ + + { + if tok != fflib.FFTok_bool && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for bool", tok)) + } + } + + { + if tok == fflib.FFTok_null { + + } else { + tmpb := fs.Output.Bytes() + + if bytes.Compare([]byte{'t', 'r', 'u', 'e'}, tmpb) == 0 { + + j.Bool = true + + } else if bytes.Compare([]byte{'f', 'a', 'l', 's', 'e'}, tmpb) == 0 { + + j.Bool = false + + } else { + err = errors.New("unexpected bytes for true/false value") + return fs.WrapErr(err) + } + + } + } + + state = fflib.FFParse_after_value + goto mainparse + +handle_Duration: + + /* handler: j.Duration type=time.Duration kind=int64 quoted=false*/ + + { + if tok != fflib.FFTok_integer && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for Duration", tok)) + } + } + + { + + if tok == fflib.FFTok_null { + + } else { + + tval, err := fflib.ParseInt(fs.Output.Bytes(), 10, 64) + + if err != nil { + return fs.WrapErr(err) + } + + j.Duration = time.Duration(tval) + + } + } + + state = fflib.FFParse_after_value + goto mainparse + +handle_SString: + + /* handler: j.SString type=[]string kind=slice quoted=false*/ + + { + + { + if tok != fflib.FFTok_left_brace && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for ", tok)) + } + } + + if tok == fflib.FFTok_null { + j.SString = nil + } else { + + j.SString = []string{} + + wantVal := true + + for { + + var tmpJSString string + + tok = fs.Scan() + if tok == fflib.FFTok_error { + goto tokerror + } + if tok == fflib.FFTok_right_brace { + break + } + + if tok == fflib.FFTok_comma { + if wantVal == true { + // TODO(pquerna): this isn't an ideal error message, this handles + // things like [,,,] as an array value. + return fs.WrapErr(fmt.Errorf("wanted value token, but got token: %v", tok)) + } + continue + } else { + wantVal = true + } + + /* handler: tmpJSString type=string kind=string quoted=false*/ + + { + + { + if tok != fflib.FFTok_string && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for string", tok)) + } + } + + if tok == fflib.FFTok_null { + + } else { + + outBuf := fs.Output.Bytes() + + tmpJSString = string(string(outBuf)) + + } + } + + j.SString = append(j.SString, tmpJSString) + + wantVal = false + } + } + } + + state = fflib.FFParse_after_value + goto mainparse + +handle_SInt: + + /* handler: j.SInt type=[]int kind=slice quoted=false*/ + + { + + { + if tok != fflib.FFTok_left_brace && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for ", tok)) + } + } + + if tok == fflib.FFTok_null { + j.SInt = nil + } else { + + j.SInt = []int{} + + wantVal := true + + for { + + var tmpJSInt int + + tok = fs.Scan() + if tok == fflib.FFTok_error { + goto tokerror + } + if tok == fflib.FFTok_right_brace { + break + } + + if tok == fflib.FFTok_comma { + if wantVal == true { + // TODO(pquerna): this isn't an ideal error message, this handles + // things like [,,,] as an array value. + return fs.WrapErr(fmt.Errorf("wanted value token, but got token: %v", tok)) + } + continue + } else { + wantVal = true + } + + /* handler: tmpJSInt type=int kind=int quoted=false*/ + + { + if tok != fflib.FFTok_integer && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for int", tok)) + } + } + + { + + if tok == fflib.FFTok_null { + + } else { + + tval, err := fflib.ParseInt(fs.Output.Bytes(), 10, 64) + + if err != nil { + return fs.WrapErr(err) + } + + tmpJSInt = int(tval) + + } + } + + j.SInt = append(j.SInt, tmpJSInt) + + wantVal = false + } + } + } + + state = fflib.FFParse_after_value + goto mainparse + +handle_SInt8: + + /* handler: j.SInt8 type=[]int8 kind=slice quoted=false*/ + + { + + { + if tok != fflib.FFTok_left_brace && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for ", tok)) + } + } + + if tok == fflib.FFTok_null { + j.SInt8 = nil + } else { + + j.SInt8 = []int8{} + + wantVal := true + + for { + + var tmpJSInt8 int8 + + tok = fs.Scan() + if tok == fflib.FFTok_error { + goto tokerror + } + if tok == fflib.FFTok_right_brace { + break + } + + if tok == fflib.FFTok_comma { + if wantVal == true { + // TODO(pquerna): this isn't an ideal error message, this handles + // things like [,,,] as an array value. + return fs.WrapErr(fmt.Errorf("wanted value token, but got token: %v", tok)) + } + continue + } else { + wantVal = true + } + + /* handler: tmpJSInt8 type=int8 kind=int8 quoted=false*/ + + { + if tok != fflib.FFTok_integer && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for int8", tok)) + } + } + + { + + if tok == fflib.FFTok_null { + + } else { + + tval, err := fflib.ParseInt(fs.Output.Bytes(), 10, 8) + + if err != nil { + return fs.WrapErr(err) + } + + tmpJSInt8 = int8(tval) + + } + } + + j.SInt8 = append(j.SInt8, tmpJSInt8) + + wantVal = false + } + } + } + + state = fflib.FFParse_after_value + goto mainparse + +handle_SInt16: + + /* handler: j.SInt16 type=[]int16 kind=slice quoted=false*/ + + { + + { + if tok != fflib.FFTok_left_brace && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for ", tok)) + } + } + + if tok == fflib.FFTok_null { + j.SInt16 = nil + } else { + + j.SInt16 = []int16{} + + wantVal := true + + for { + + var tmpJSInt16 int16 + + tok = fs.Scan() + if tok == fflib.FFTok_error { + goto tokerror + } + if tok == fflib.FFTok_right_brace { + break + } + + if tok == fflib.FFTok_comma { + if wantVal == true { + // TODO(pquerna): this isn't an ideal error message, this handles + // things like [,,,] as an array value. + return fs.WrapErr(fmt.Errorf("wanted value token, but got token: %v", tok)) + } + continue + } else { + wantVal = true + } + + /* handler: tmpJSInt16 type=int16 kind=int16 quoted=false*/ + + { + if tok != fflib.FFTok_integer && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for int16", tok)) + } + } + + { + + if tok == fflib.FFTok_null { + + } else { + + tval, err := fflib.ParseInt(fs.Output.Bytes(), 10, 16) + + if err != nil { + return fs.WrapErr(err) + } + + tmpJSInt16 = int16(tval) + + } + } + + j.SInt16 = append(j.SInt16, tmpJSInt16) + + wantVal = false + } + } + } + + state = fflib.FFParse_after_value + goto mainparse + +handle_SInt32: + + /* handler: j.SInt32 type=[]int32 kind=slice quoted=false*/ + + { + + { + if tok != fflib.FFTok_left_brace && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for ", tok)) + } + } + + if tok == fflib.FFTok_null { + j.SInt32 = nil + } else { + + j.SInt32 = []int32{} + + wantVal := true + + for { + + var tmpJSInt32 int32 + + tok = fs.Scan() + if tok == fflib.FFTok_error { + goto tokerror + } + if tok == fflib.FFTok_right_brace { + break + } + + if tok == fflib.FFTok_comma { + if wantVal == true { + // TODO(pquerna): this isn't an ideal error message, this handles + // things like [,,,] as an array value. + return fs.WrapErr(fmt.Errorf("wanted value token, but got token: %v", tok)) + } + continue + } else { + wantVal = true + } + + /* handler: tmpJSInt32 type=int32 kind=int32 quoted=false*/ + + { + if tok != fflib.FFTok_integer && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for int32", tok)) + } + } + + { + + if tok == fflib.FFTok_null { + + } else { + + tval, err := fflib.ParseInt(fs.Output.Bytes(), 10, 32) + + if err != nil { + return fs.WrapErr(err) + } + + tmpJSInt32 = int32(tval) + + } + } + + j.SInt32 = append(j.SInt32, tmpJSInt32) + + wantVal = false + } + } + } + + state = fflib.FFParse_after_value + goto mainparse + +handle_SInt64: + + /* handler: j.SInt64 type=[]int64 kind=slice quoted=false*/ + + { + + { + if tok != fflib.FFTok_left_brace && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for ", tok)) + } + } + + if tok == fflib.FFTok_null { + j.SInt64 = nil + } else { + + j.SInt64 = []int64{} + + wantVal := true + + for { + + var tmpJSInt64 int64 + + tok = fs.Scan() + if tok == fflib.FFTok_error { + goto tokerror + } + if tok == fflib.FFTok_right_brace { + break + } + + if tok == fflib.FFTok_comma { + if wantVal == true { + // TODO(pquerna): this isn't an ideal error message, this handles + // things like [,,,] as an array value. + return fs.WrapErr(fmt.Errorf("wanted value token, but got token: %v", tok)) + } + continue + } else { + wantVal = true + } + + /* handler: tmpJSInt64 type=int64 kind=int64 quoted=false*/ + + { + if tok != fflib.FFTok_integer && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for int64", tok)) + } + } + + { + + if tok == fflib.FFTok_null { + + } else { + + tval, err := fflib.ParseInt(fs.Output.Bytes(), 10, 64) + + if err != nil { + return fs.WrapErr(err) + } + + tmpJSInt64 = int64(tval) + + } + } + + j.SInt64 = append(j.SInt64, tmpJSInt64) + + wantVal = false + } + } + } + + state = fflib.FFParse_after_value + goto mainparse + +handle_SFloat32: + + /* handler: j.SFloat32 type=[]float32 kind=slice quoted=false*/ + + { + + { + if tok != fflib.FFTok_left_brace && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for ", tok)) + } + } + + if tok == fflib.FFTok_null { + j.SFloat32 = nil + } else { + + j.SFloat32 = []float32{} + + wantVal := true + + for { + + var tmpJSFloat32 float32 + + tok = fs.Scan() + if tok == fflib.FFTok_error { + goto tokerror + } + if tok == fflib.FFTok_right_brace { + break + } + + if tok == fflib.FFTok_comma { + if wantVal == true { + // TODO(pquerna): this isn't an ideal error message, this handles + // things like [,,,] as an array value. + return fs.WrapErr(fmt.Errorf("wanted value token, but got token: %v", tok)) + } + continue + } else { + wantVal = true + } + + /* handler: tmpJSFloat32 type=float32 kind=float32 quoted=false*/ + + { + if tok != fflib.FFTok_double && tok != fflib.FFTok_integer && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for float32", tok)) + } + } + + { + + if tok == fflib.FFTok_null { + + } else { + + tval, err := fflib.ParseFloat(fs.Output.Bytes(), 32) + + if err != nil { + return fs.WrapErr(err) + } + + tmpJSFloat32 = float32(tval) + + } + } + + j.SFloat32 = append(j.SFloat32, tmpJSFloat32) + + wantVal = false + } + } + } + + state = fflib.FFParse_after_value + goto mainparse + +handle_SFloat64: + + /* handler: j.SFloat64 type=[]float64 kind=slice quoted=false*/ + + { + + { + if tok != fflib.FFTok_left_brace && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for ", tok)) + } + } + + if tok == fflib.FFTok_null { + j.SFloat64 = nil + } else { + + j.SFloat64 = []float64{} + + wantVal := true + + for { + + var tmpJSFloat64 float64 + + tok = fs.Scan() + if tok == fflib.FFTok_error { + goto tokerror + } + if tok == fflib.FFTok_right_brace { + break + } + + if tok == fflib.FFTok_comma { + if wantVal == true { + // TODO(pquerna): this isn't an ideal error message, this handles + // things like [,,,] as an array value. + return fs.WrapErr(fmt.Errorf("wanted value token, but got token: %v", tok)) + } + continue + } else { + wantVal = true + } + + /* handler: tmpJSFloat64 type=float64 kind=float64 quoted=false*/ + + { + if tok != fflib.FFTok_double && tok != fflib.FFTok_integer && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for float64", tok)) + } + } + + { + + if tok == fflib.FFTok_null { + + } else { + + tval, err := fflib.ParseFloat(fs.Output.Bytes(), 64) + + if err != nil { + return fs.WrapErr(err) + } + + tmpJSFloat64 = float64(tval) + + } + } + + j.SFloat64 = append(j.SFloat64, tmpJSFloat64) + + wantVal = false + } + } + } + + state = fflib.FFParse_after_value + goto mainparse + +handle_SBool: + + /* handler: j.SBool type=[]bool kind=slice quoted=false*/ + + { + + { + if tok != fflib.FFTok_left_brace && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for ", tok)) + } + } + + if tok == fflib.FFTok_null { + j.SBool = nil + } else { + + j.SBool = []bool{} + + wantVal := true + + for { + + var tmpJSBool bool + + tok = fs.Scan() + if tok == fflib.FFTok_error { + goto tokerror + } + if tok == fflib.FFTok_right_brace { + break + } + + if tok == fflib.FFTok_comma { + if wantVal == true { + // TODO(pquerna): this isn't an ideal error message, this handles + // things like [,,,] as an array value. + return fs.WrapErr(fmt.Errorf("wanted value token, but got token: %v", tok)) + } + continue + } else { + wantVal = true + } + + /* handler: tmpJSBool type=bool kind=bool quoted=false*/ + + { + if tok != fflib.FFTok_bool && tok != fflib.FFTok_null { + return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for bool", tok)) + } + } + + { + if tok == fflib.FFTok_null { + + } else { + tmpb := fs.Output.Bytes() + + if bytes.Compare([]byte{'t', 'r', 'u', 'e'}, tmpb) == 0 { + + tmpJSBool = true + + } else if bytes.Compare([]byte{'f', 'a', 'l', 's', 'e'}, tmpb) == 0 { + + tmpJSBool = false + + } else { + err = errors.New("unexpected bytes for true/false value") + return fs.WrapErr(err) + } + + } + } + + j.SBool = append(j.SBool, tmpJSBool) + + wantVal = false + } + } + } + + state = fflib.FFParse_after_value + goto mainparse + +handle_Struct: + + /* handler: j.Struct type=ffjson.AStruct kind=struct quoted=false*/ + + { + if tok == fflib.FFTok_null { + + } else { + + err = j.Struct.UnmarshalJSONFFLexer(fs, fflib.FFParse_want_key) + if err != nil { + return err + } + } + state = fflib.FFParse_after_value + } + + state = fflib.FFParse_after_value + goto mainparse + +wantedvalue: + return fs.WrapErr(fmt.Errorf("wanted value token, but got token: %v", tok)) +wrongtokenerror: + return fs.WrapErr(fmt.Errorf("ffjson: wanted token: %v, but got token: %v output=%s", wantedTok, tok, fs.Output.String())) +tokerror: + if fs.BigError != nil { + return fs.WrapErr(fs.BigError) + } + err = fs.Error.ToError() + if err != nil { + return fs.WrapErr(err) + } + panic("ffjson-generated: unreachable, please report bug.") +done: + + return nil +} diff --git a/utils/serialization/json/jsontest/helpers_test.go b/utils/serialization/json/jsontest/helpers_test.go new file mode 100644 index 0000000000..09532737e0 --- /dev/null +++ b/utils/serialization/json/jsontest/helpers_test.go @@ -0,0 +1,69 @@ +//nolint:misspell // serialization package names and aliases are intentional. +package jsontest + +import ( + "testing" + + "github.com/go-faker/faker/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + jsonserialization "github.com/ARM-software/golang-utils/utils/serialization/json" //nolint:misspell + "github.com/ARM-software/golang-utils/utils/serialization/json/jsontest/easyjson" //nolint:misspell + "github.com/ARM-software/golang-utils/utils/serialization/json/jsontest/ffjson" //nolint:misspell + "github.com/ARM-software/golang-utils/utils/serialization/json/jsontest/nofast" //nolint:misspell +) + +type equals interface { + Equals(any) bool +} + +func TestFastJSONMarshalling(t *testing.T) { + tests := []struct { + name string + test1 equals + test2 equals + }{ + { + name: "no fast json", + test1: &nofast.TestingStruct{}, + test2: &nofast.TestingStruct{}, + }, + { + name: "ffjson", + test1: &ffjson.TestingStruct{}, + test2: &ffjson.TestingStruct{}, + }, + { + name: "easyjson", + test1: &easyjson.TestingStruct{}, + test2: &easyjson.TestingStruct{}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + testStruct0 := test.test1 + require.NoError(t, faker.FakeData(testStruct0)) + + testStruct1 := test.test2 + assert.False(t, testStruct0.Equals(testStruct1)) + assert.False(t, testStruct1.Equals(testStruct0)) + + encoded, err := jsonserialization.Marshal(testStruct0) + require.NoError(t, err) + + err = jsonserialization.Unmarshal(encoded, testStruct1) + require.NoError(t, err) + + assert.True(t, testStruct0.Equals(testStruct1)) + assert.True(t, testStruct1.Equals(testStruct0)) + }) + } +} + +func TestJSONMarshallingNil(t *testing.T) { + value, err := jsonserialization.Marshal(nil) + require.NoError(t, err) + assert.NotEmpty(t, value) +} diff --git a/utils/serialization/json/jsontest/nofast/testing_types.go b/utils/serialization/json/jsontest/nofast/testing_types.go new file mode 100644 index 0000000000..d93b065020 --- /dev/null +++ b/utils/serialization/json/jsontest/nofast/testing_types.go @@ -0,0 +1,136 @@ +package nofast + +import ( + "slices" + "time" + + "github.com/ARM-software/golang-utils/utils/collection" +) + +type TestingStruct struct { + Int int + Int8 int8 + Int16 int16 + Int32 int32 + Int64 int64 + String string + Bool bool + Duration time.Duration + SString []string + SInt []int + SInt8 []int8 + SInt16 []int16 + SInt32 []int32 + SInt64 []int64 + SFloat32 []float32 + SFloat64 []float64 + SBool []bool + Struct AStruct +} + +func (s *TestingStruct) Equals(other any) bool { + if s == other { + return true + } + + if other == nil { + return s == nil + } + + if o, ok := other.(*TestingStruct); ok { + return collection.All([]bool{ + s.Int == o.Int, + s.Int8 == o.Int8, + s.Int16 == o.Int16, + s.Int32 == o.Int32, + s.Int64 == o.Int64, + s.String == o.String, + s.Bool == o.Bool, + s.Duration == o.Duration, + slices.Equal(s.SString, o.SString), + slices.Equal(s.SInt, o.SInt), + slices.Equal(s.SInt8, o.SInt8), + slices.Equal(s.SInt16, o.SInt16), + slices.Equal(s.SInt32, o.SInt32), + slices.Equal(s.SInt64, o.SInt64), + slices.Equal(s.SFloat32, o.SFloat32), + slices.Equal(s.SFloat64, o.SFloat64), + slices.Equal(s.SBool, o.SBool), + s.Struct.Equals(o.Struct), + }) + } + + if o, ok := other.(TestingStruct); ok { + return collection.All([]bool{ + s.Int == o.Int, + s.Int8 == o.Int8, + s.Int16 == o.Int16, + s.Int32 == o.Int32, + s.Int64 == o.Int64, + s.String == o.String, + s.Bool == o.Bool, + slices.Equal(s.SString, o.SString), + slices.Equal(s.SInt, o.SInt), + slices.Equal(s.SInt8, o.SInt8), + slices.Equal(s.SInt16, o.SInt16), + slices.Equal(s.SInt32, o.SInt32), + slices.Equal(s.SInt64, o.SInt64), + slices.Equal(s.SFloat32, o.SFloat32), + slices.Equal(s.SFloat64, o.SFloat64), + slices.Equal(s.SBool, o.SBool), + s.Struct.Equals(o.Struct), + }) + } + + return false +} + +type AStruct struct { + Number int64 + Height int64 + AnotherStruct BStruct +} + +func (s *AStruct) Equals(other any) bool { + if s == other { + return true + } + + if other == nil { + return s == nil + } + + if o, ok := other.(*AStruct); ok { + return s.Number == o.Number && s.Height == o.Height && s.AnotherStruct.Equals(&o.AnotherStruct) + } + + if o, ok := other.(AStruct); ok { + return s.Number == o.Number && s.Height == o.Height && s.AnotherStruct.Equals(&o.AnotherStruct) + } + + return false +} + +type BStruct struct { + Image string +} + +func (s *BStruct) Equals(other any) bool { + if s == other { + return true + } + + if other == nil { + return s == nil + } + + if o, ok := other.(*BStruct); ok { + return s.Image == o.Image + } + + if o, ok := other.(BStruct); ok { + return s.Image == o.Image + } + + return false +} diff --git a/utils/serialization/xml.go b/utils/serialization/xml.go index 8a0c142f50..58d87a05bd 100644 --- a/utils/serialization/xml.go +++ b/utils/serialization/xml.go @@ -14,7 +14,7 @@ import ( // UnmarshallXML was introduced instead // of using xml.Unmarshal() as this only supports UTF8 // But it's been noticed that UnmarshalXml doesn't support UTF16 -func UnmarshallXML(data []byte, value interface{}) error { +func UnmarshallXML(data []byte, value any) error { // Read the XML file and create an in-memory model constructed from the // elements in the data reader := bytes.NewReader(data) diff --git a/utils/serialization/yaml/decoder.go b/utils/serialization/yaml/decoder.go new file mode 100644 index 0000000000..c9e4ae4f22 --- /dev/null +++ b/utils/serialization/yaml/decoder.go @@ -0,0 +1,41 @@ +package yaml + +import ( + "context" + "io" + + "github.com/ARM-software/golang-utils/utils/safeio" +) + +// Decoder reads YAML from a reader and decodes it into Go values. +// It follows the same helper shape as the repository's JSON `Decoder`. +// +// Reads are context-aware, and the input is converted to JSON before the JSON +// helpers are used, so the repository keeps a single fast decoding path. +// This should not be used by more than one goroutine at a time. +type Decoder struct { + ctx context.Context + reader io.Reader +} + +// NewDecoder creates a Decoder that reads YAML values from r. +// It follows the same helper shape as the repository's JSON `NewDecoder` +// helper. +func NewDecoder(ctx context.Context, r io.Reader) *Decoder { + return &Decoder{ctx: ctx, reader: safeio.NewContextualReader(ctx, r)} +} + +// Decode reads YAML from the decoder and stores the result in v. +// It follows the same helper shape as the repository's JSON `Decoder.Decode` +// method. +// +// The data is converted to JSON first, then passed to the JSON helpers, so the +// same fast and implementation-agnostic decoding logic is reused here. +func (d *Decoder) Decode(v any) error { + data, err := safeio.ReadAll(d.ctx, d.reader) + if err != nil { + return err + } + + return Unmarshal(data, v) +} diff --git a/utils/serialization/yaml/encoder.go b/utils/serialization/yaml/encoder.go new file mode 100644 index 0000000000..cc3e2dbf06 --- /dev/null +++ b/utils/serialization/yaml/encoder.go @@ -0,0 +1,42 @@ +package yaml + +import ( + "context" + "io" + + "github.com/ARM-software/golang-utils/utils/safeio" +) + +// Encoder writes YAML values to a writer. +// It follows the same helper shape as the repository's JSON `Encoder`. +// +// Writes are context-aware. Values are first encoded through the JSON helpers +// so any generated fast JSON serialisers can still be used before converting +// the JSON output to YAML. +// This should not be used by more than one goroutine at a time. +type Encoder struct { + writer io.Writer +} + +// NewEncoder creates an Encoder that writes YAML values to w. +// It follows the same helper shape as the repository's JSON `NewEncoder` +// helper. +func NewEncoder(ctx context.Context, w io.Writer) *Encoder { + return &Encoder{writer: safeio.ContextualWriter(ctx, w)} +} + +// Encode writes v as YAML to the encoder's writer. +// It follows the same helper shape as the repository's JSON `Encoder.Encode` +// method. +// +// The value is first encoded through the JSON helpers, then the JSON output is +// converted to YAML before it is written. +func (e *Encoder) Encode(v any) error { + data, err := Marshal(v) + if err != nil { + return err + } + + _, err = e.writer.Write(data) + return err +} diff --git a/utils/serialization/yaml/helpers.go b/utils/serialization/yaml/helpers.go new file mode 100644 index 0000000000..c6a8d05671 --- /dev/null +++ b/utils/serialization/yaml/helpers.go @@ -0,0 +1,93 @@ +// Package yaml mirrors the repository's JSON helper shape for YAML data. +// +// The package converts YAML input to JSON before delegating to the JSON helpers, +// which keeps one implementation of the fast, non-reflective JSON decoding +// paths and their context-aware stream handling. On the write path, it encodes +// to JSON first, then converts that JSON to YAML. +// +// YAML parsing support comes from sigs.k8s.io/yaml, which uses yaml/go-yaml +// underneath. In practice that means the package supports most of YAML 1.2 +// while preserving some YAML 1.1 behaviour for compatibility, including YAML +// aliases and anchors, as documented at +// https://github.com/yaml/go-yaml#compatibility. +// +//nolint:misspell // serialization package names and aliases are intentional. +package yaml + +import ( + "bytes" + "context" + + sigsyaml "sigs.k8s.io/yaml" + + "github.com/ARM-software/golang-utils/utils/commonerrors" + jsonserialization "github.com/ARM-software/golang-utils/utils/serialization/json" //nolint:misspell +) + +// ToJSON converts YAML data to JSON. +// +// YAML parsing support comes from sigs.k8s.io/yaml, which uses yaml/go-yaml +// underneath. In practice that means it supports most of YAML 1.2 while +// preserving some YAML 1.1 behaviour for compatibility, as documented at +// https://github.com/yaml/go-yaml#compatibility. That includes support for +// YAML aliases and anchors. +// See also https://github.com/yaml/go-yaml. +func ToJSON(yaml []byte) (json []byte, err error) { + json, err = sigsyaml.YAMLToJSON(yaml) + if err != nil { + err = commonerrors.WrapError(commonerrors.ErrMarshalling, err, "failed converting YAML to JSON") + } + return +} + +// Marshal encodes a value to a YAML byte slice. +// It follows the same helper shape as the repository's JSON `Marshal` helper. +// +// Values are first encoded through the JSON helpers so any generated fast JSON +// serialisers can still be used, then the resulting JSON is converted to YAML. +func Marshal(v any) ([]byte, error) { + jsonData, err := jsonserialization.Marshal(v) + if err != nil { + return nil, err + } + + return jsonserialization.ToYAML(jsonData) +} + +// MarshalWithContext encodes a value to a YAML byte slice using a context-aware +// Encoder. +// It follows the same helper shape as Marshal, but routes the write through the +// package's context-aware streaming helpers. +func MarshalWithContext(ctx context.Context, v any) (content []byte, err error) { + var buf bytes.Buffer + encoder := NewEncoder(ctx, &buf) + err = encoder.Encode(v) + if err != nil { + return + } + content = buf.Bytes() + return +} + +// Unmarshal decodes a YAML byte slice into a destination value. +// It follows the same helper shape as the repository's JSON `Unmarshal` +// helper. +// +// YAML is converted to JSON first, so the JSON helpers can retain their fast- +// generated decoding paths and shared decoding behaviour. +func Unmarshal(data []byte, v any) error { + jsonData, err := ToJSON(data) + if err != nil { + return err + } + + return jsonserialization.Unmarshal(jsonData, v) +} + +// UnmarshallWithContext decodes a YAML byte slice into a destination value +// using a context-aware Decoder. +// It follows the same helper shape as Unmarshal, but routes the read through +// the package's context-aware streaming helpers. +func UnmarshallWithContext(ctx context.Context, data []byte, v any) error { + return NewDecoder(ctx, bytes.NewReader(data)).Decode(v) +} diff --git a/utils/serialization/yaml/helpers_test.go b/utils/serialization/yaml/helpers_test.go new file mode 100644 index 0000000000..7315ad6842 --- /dev/null +++ b/utils/serialization/yaml/helpers_test.go @@ -0,0 +1,95 @@ +package yaml + +import ( + "bytes" + "context" + "testing" + + "github.com/go-faker/faker/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ARM-software/golang-utils/utils/commonerrors" + "github.com/ARM-software/golang-utils/utils/commonerrors/errortest" +) + +type TestStruct struct { + Elem1 string + Elem2 int64 + Elem3 string + Elem4 bool + Elem5 float64 +} + +func TestMarshalAndUnmarshal(t *testing.T) { + test := TestStruct{ + Elem1: faker.Sentence(), + Elem2: 456, + Elem3: faker.Paragraph(), + Elem4: true, + Elem5: 0.415465464565464, + } + + encoded, err := Marshal(&test) + require.NoError(t, err) + assert.Contains(t, string(encoded), "Elem1:") + + var decoded TestStruct + err = Unmarshal(encoded, &decoded) + require.NoError(t, err) + assert.Equal(t, test, decoded) +} + +func TestEncoderAndDecoder(t *testing.T) { + test := TestStruct{ + Elem1: faker.Sentence(), + Elem2: 456, + Elem3: faker.Paragraph(), + Elem4: true, + Elem5: 0.415465545454464565464, + } + + var buf bytes.Buffer + encoder := NewEncoder(context.Background(), &buf) + err := encoder.Encode(test) + require.NoError(t, err) + + var decoded TestStruct + decoder := NewDecoder(context.Background(), &buf) + err = decoder.Decode(&decoded) + require.NoError(t, err) + assert.Equal(t, test, decoded) +} + +func TestMarshalAndUnmarshallWithContext(t *testing.T) { + test := TestStruct{ + Elem1: faker.Sentence(), + Elem2: 456, + Elem3: faker.Paragraph(), + Elem4: true, + Elem5: 0.415465464565464, + } + + encoded, err := MarshalWithContext(context.Background(), &test) + require.NoError(t, err) + + var decoded TestStruct + err = UnmarshallWithContext(context.Background(), encoded, &decoded) + require.NoError(t, err) + assert.Equal(t, test, decoded) +} + +func TestToJSON(t *testing.T) { + input := []byte("name: value\ncount: 2\n") + + output, err := ToJSON(input) + require.NoError(t, err) + + assert.JSONEq(t, `{"count":2,"name":"value"}`, string(output)) +} + +func TestToJSONInvalidYAML(t *testing.T) { + _, err := ToJSON([]byte("name: [value\n")) + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrMarshalling) +} diff --git a/utils/serialization/yaml/yamltest/helpers_test.go b/utils/serialization/yaml/yamltest/helpers_test.go new file mode 100644 index 0000000000..6c1ab4ba44 --- /dev/null +++ b/utils/serialization/yaml/yamltest/helpers_test.go @@ -0,0 +1,126 @@ +//nolint:misspell // serialization package names and aliases are intentional. +package yamltest + +import ( + "bytes" + "context" + "path" + "testing" + + "github.com/go-faker/faker/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ARM-software/golang-utils/utils/filesystem" + yamlserialization "github.com/ARM-software/golang-utils/utils/serialization/yaml" //nolint:misspell + "github.com/ARM-software/golang-utils/utils/serialization/yaml/yamltest/nofast" //nolint:misspell +) + +func TestYAMLMarshalling(t *testing.T) { + testStruct0 := &nofast.TestingStruct{} + require.NoError(t, faker.FakeData(testStruct0)) + + testStruct1 := &nofast.TestingStruct{} + assert.False(t, testStruct0.Equals(testStruct1)) + + encoded, err := yamlserialization.Marshal(testStruct0) + require.NoError(t, err) + + err = yamlserialization.Unmarshal(encoded, testStruct1) + require.NoError(t, err) + + assert.True(t, testStruct0.Equals(testStruct1)) + assert.True(t, testStruct1.Equals(testStruct0)) +} + +func TestYAMLMarshallingEncoding(t *testing.T) { + test := &nofast.TestingStruct{} + require.NoError(t, faker.FakeData(test)) + + var buf bytes.Buffer + encoder := yamlserialization.NewEncoder(context.Background(), &buf) + err := encoder.Encode(test) + require.NoError(t, err) + + decoded := &nofast.TestingStruct{} + decoder := yamlserialization.NewDecoder(context.Background(), &buf) + err = decoder.Decode(decoded) + require.NoError(t, err) + + assert.True(t, test.Equals(decoded)) + assert.True(t, decoded.Equals(test)) +} + +func TestYAMLMarshallingNil(t *testing.T) { + value, err := yamlserialization.Marshal(nil) + require.NoError(t, err) + assert.NotEmpty(t, value) +} + +func TestYAMLFixtures(t *testing.T) { + tests := []struct { + name string + fileName string + assertFn func(t *testing.T, data []byte) + }{ + { + name: "anchor as key with alias", + fileName: "anchor_as_key_with_alias.yaml", + assertFn: func(t *testing.T, data []byte) { + actual := map[string]any{} + require.NoError(t, yamlserialization.Unmarshal(data, &actual)) + assert.Equal(t, map[string]any{"foo": "bar", "bar": "quz"}, actual) + }, + }, + { + name: "alias reuse", + fileName: "alias_reuse.yaml", + assertFn: func(t *testing.T, data []byte) { + actual := map[string]any{} + require.NoError(t, yamlserialization.Unmarshal(data, &actual)) + assert.Equal(t, map[string]any{ + "First occurrence": "Foo", + "Second occurrence": "Foo", + "Override anchor": "Bar", + "Reuse anchor": "Bar", + }, actual) + }, + }, + { + name: "yaml 1.1 bool compatibility", + fileName: "yaml11_bool_compat.yaml", + assertFn: func(t *testing.T, data []byte) { + actual := map[string]bool{} + require.NoError(t, yamlserialization.Unmarshal(data, &actual)) + assert.Equal(t, map[string]bool{"option": true}, actual) + }, + }, + { + name: "anchor sequence alias", + fileName: "anchor_sequence_alias.yaml", + assertFn: func(t *testing.T, data []byte) { + actual := map[string][]int{} + require.NoError(t, yamlserialization.Unmarshal(data, &actual)) + assert.Equal(t, map[string][]int{"a": {1, 2}, "b": {1, 2}}, actual) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + data, err := filesystem.ReadFile(path.Join("testdata", test.fileName)) + require.NoError(t, err) + test.assertFn(t, data) + }) + } +} + +func TestToJSONFixtures(t *testing.T) { + data, err := filesystem.ReadFile(path.Join("testdata", "anchor_as_key_with_alias.yaml")) + require.NoError(t, err) + + jsonData, err := yamlserialization.ToJSON(data) + require.NoError(t, err) + + assert.JSONEq(t, `{"bar":"quz","foo":"bar"}`, string(jsonData)) +} diff --git a/utils/serialization/yaml/yamltest/nofast/testing_types.go b/utils/serialization/yaml/yamltest/nofast/testing_types.go new file mode 100644 index 0000000000..d93b065020 --- /dev/null +++ b/utils/serialization/yaml/yamltest/nofast/testing_types.go @@ -0,0 +1,136 @@ +package nofast + +import ( + "slices" + "time" + + "github.com/ARM-software/golang-utils/utils/collection" +) + +type TestingStruct struct { + Int int + Int8 int8 + Int16 int16 + Int32 int32 + Int64 int64 + String string + Bool bool + Duration time.Duration + SString []string + SInt []int + SInt8 []int8 + SInt16 []int16 + SInt32 []int32 + SInt64 []int64 + SFloat32 []float32 + SFloat64 []float64 + SBool []bool + Struct AStruct +} + +func (s *TestingStruct) Equals(other any) bool { + if s == other { + return true + } + + if other == nil { + return s == nil + } + + if o, ok := other.(*TestingStruct); ok { + return collection.All([]bool{ + s.Int == o.Int, + s.Int8 == o.Int8, + s.Int16 == o.Int16, + s.Int32 == o.Int32, + s.Int64 == o.Int64, + s.String == o.String, + s.Bool == o.Bool, + s.Duration == o.Duration, + slices.Equal(s.SString, o.SString), + slices.Equal(s.SInt, o.SInt), + slices.Equal(s.SInt8, o.SInt8), + slices.Equal(s.SInt16, o.SInt16), + slices.Equal(s.SInt32, o.SInt32), + slices.Equal(s.SInt64, o.SInt64), + slices.Equal(s.SFloat32, o.SFloat32), + slices.Equal(s.SFloat64, o.SFloat64), + slices.Equal(s.SBool, o.SBool), + s.Struct.Equals(o.Struct), + }) + } + + if o, ok := other.(TestingStruct); ok { + return collection.All([]bool{ + s.Int == o.Int, + s.Int8 == o.Int8, + s.Int16 == o.Int16, + s.Int32 == o.Int32, + s.Int64 == o.Int64, + s.String == o.String, + s.Bool == o.Bool, + slices.Equal(s.SString, o.SString), + slices.Equal(s.SInt, o.SInt), + slices.Equal(s.SInt8, o.SInt8), + slices.Equal(s.SInt16, o.SInt16), + slices.Equal(s.SInt32, o.SInt32), + slices.Equal(s.SInt64, o.SInt64), + slices.Equal(s.SFloat32, o.SFloat32), + slices.Equal(s.SFloat64, o.SFloat64), + slices.Equal(s.SBool, o.SBool), + s.Struct.Equals(o.Struct), + }) + } + + return false +} + +type AStruct struct { + Number int64 + Height int64 + AnotherStruct BStruct +} + +func (s *AStruct) Equals(other any) bool { + if s == other { + return true + } + + if other == nil { + return s == nil + } + + if o, ok := other.(*AStruct); ok { + return s.Number == o.Number && s.Height == o.Height && s.AnotherStruct.Equals(&o.AnotherStruct) + } + + if o, ok := other.(AStruct); ok { + return s.Number == o.Number && s.Height == o.Height && s.AnotherStruct.Equals(&o.AnotherStruct) + } + + return false +} + +type BStruct struct { + Image string +} + +func (s *BStruct) Equals(other any) bool { + if s == other { + return true + } + + if other == nil { + return s == nil + } + + if o, ok := other.(*BStruct); ok { + return s.Image == o.Image + } + + if o, ok := other.(BStruct); ok { + return s.Image == o.Image + } + + return false +} diff --git a/utils/serialization/yaml/yamltest/testdata/alias_reuse.yaml b/utils/serialization/yaml/yamltest/testdata/alias_reuse.yaml new file mode 100644 index 0000000000..c035022708 --- /dev/null +++ b/utils/serialization/yaml/yamltest/testdata/alias_reuse.yaml @@ -0,0 +1,6 @@ +# Source: https://github.com/yaml/go-yaml/tree/main/testdata/decode.yaml +# Case: alias reuse (yaml-test-suite 3GZX) +First occurrence: &anchor Foo +Second occurrence: *anchor +Override anchor: &anchor Bar +Reuse anchor: *anchor diff --git a/utils/serialization/yaml/yamltest/testdata/anchor_as_key_with_alias.yaml b/utils/serialization/yaml/yamltest/testdata/anchor_as_key_with_alias.yaml new file mode 100644 index 0000000000..ef902ca687 --- /dev/null +++ b/utils/serialization/yaml/yamltest/testdata/anchor_as_key_with_alias.yaml @@ -0,0 +1,4 @@ +# Source: https://github.com/yaml/go-yaml/tree/main/testdata/decode.yaml +# Case: anchor as key with alias +foo: &bar bar +*bar : quz diff --git a/utils/serialization/yaml/yamltest/testdata/anchor_sequence_alias.yaml b/utils/serialization/yaml/yamltest/testdata/anchor_sequence_alias.yaml new file mode 100644 index 0000000000..5e5caa2ec2 --- /dev/null +++ b/utils/serialization/yaml/yamltest/testdata/anchor_sequence_alias.yaml @@ -0,0 +1,4 @@ +# Source: https://github.com/yaml/go-yaml/tree/main/testdata/decode.yaml +# Case: struct field B with anchor list +a: &a [1, 2] +b: *a diff --git a/utils/serialization/yaml/yamltest/testdata/yaml11_bool_compat.yaml b/utils/serialization/yaml/yamltest/testdata/yaml11_bool_compat.yaml new file mode 100644 index 0000000000..43feaf683d --- /dev/null +++ b/utils/serialization/yaml/yamltest/testdata/yaml11_bool_compat.yaml @@ -0,0 +1,3 @@ +# Source: https://github.com/yaml/go-yaml/tree/main/testdata/decode.yaml +# Case: on as bool (1.1 compat) +option: on diff --git a/utils/validation/jsonschema/interface.go b/utils/validation/jsonschema/interface.go new file mode 100644 index 0000000000..adedf2892e --- /dev/null +++ b/utils/validation/jsonschema/interface.go @@ -0,0 +1,34 @@ +package jsonschema + +import ( + "context" + + "github.com/ARM-software/golang-utils/utils/filesystem" +) + +//go:generate go tool mockgen -destination=../../mocks/mock_$GOPACKAGE.go -package=mocks github.com/ARM-software/golang-utils/utils/validation/$GOPACKAGE ISchemaValidator + +// ISchemaValidator validates content against a compiled JSON +// Schema definition. +// +// Implementations may support different source formats such as JSON or YAML, +// but all expose the same three entry points: validating already-loaded +// content, validating a file from the global filesystem, and validating a file +// from a supplied filesystem. +type ISchemaValidator interface { + // ValidateContent validates already-loaded content against the compiled + // schema definition. + ValidateContent(context.Context, any) error + // ValidateFile validates a file from the global filesystem against the + // compiled schema definition. + ValidateFile(ctx context.Context, filepath string) error + // ValidateFileWithLimits validates a file from the global filesystem against the + // compiled schema definition. + ValidateFileWithLimits(ctx context.Context, filepath string, fileLimits filesystem.ILimits) error + // ValidateFileInFS validates a file from the supplied filesystem against the + // compiled schema definition. + ValidateFileInFS(ctx context.Context, fs filesystem.FS, filepath string) error + // ValidateFileInFSWithLimits validates a file from the supplied filesystem against the + // compiled schema definition. + ValidateFileInFSWithLimits(ctx context.Context, fs filesystem.FS, filepath string, fileLimits filesystem.ILimits) error +} diff --git a/utils/validation/jsonschema/options.go b/utils/validation/jsonschema/options.go new file mode 100644 index 0000000000..067cfddbdb --- /dev/null +++ b/utils/validation/jsonschema/options.go @@ -0,0 +1,128 @@ +package jsonschema + +import ( + "strings" + + "github.com/ARM-software/golang-utils/utils/collection" + "github.com/ARM-software/golang-utils/utils/filesystem" +) + +// SchemaOption configures a Schema during construction. +type SchemaOption func(*Schema) *Schema + +// DefaultSchema returns a Schema initialised with the package defaults. +// +// The default filesystem is the global filesystem, and the schema ID is derived +// from the local path when it has not been set explicitly. +func DefaultSchema() *Schema { + return (&Schema{}).Default() +} + +// Default applies the package defaults to a Schema. +func (s *Schema) Default() *Schema { + if s == nil { + s = &Schema{} + } + if s.Filesystem == nil { + s.Filesystem = filesystem.GetGlobalFileSystem() + } + if s.Limits == nil { + s.Limits = filesystem.NoLimits() + } + s.ID = schemaID(s.ID, s.LocalPath) + return s +} + +// Apply applies a single option to a Schema and then reapplies defaults. +func (s *Schema) Apply(option SchemaOption) *Schema { + if option == nil { + return s.Default() + } + return option(s).Default() +} + +// NewJSONSchemaFile constructs a Schema from the supplied options. +// +// Options are applied in order, after which the package defaults are +// materialised. +func NewJSONSchemaFile(options ...SchemaOption) *Schema { + schema := DefaultSchema() + collection.ForEach(options, func(option SchemaOption) { + schema.Apply(option) + }) + return schema.Default() +} + +// WithTitle sets the human-readable title of a Schema. +func WithTitle(title string) SchemaOption { + return func(schema *Schema) *Schema { + if schema == nil { + schema = DefaultSchema() + } + schema.Title = strings.TrimSpace(title) + return schema + } +} + +// WithLocalPath sets the local path of a Schema file. +// +// The path is trimmed, and the schema ID is recalculated with schemaID when no +// explicit ID has been provided. +func WithLocalPath(localPath string) SchemaOption { + return func(schema *Schema) *Schema { + if schema == nil { + schema = DefaultSchema() + } + schema.LocalPath = localPath + schema.ID = schemaID(schema.ID, schema.LocalPath) + return schema + } +} + +// WithID sets the schema ID of a Schema. +// +// If the supplied ID is empty, schemaID falls back to the current local path. +func WithID(id string) SchemaOption { + return func(schema *Schema) *Schema { + if schema == nil { + schema = DefaultSchema() + } + schema.ID = schemaID(id, schema.LocalPath) + return schema + } +} + +// WithFilesystem sets the filesystem used to load the Schema file. +// +// A nil filesystem falls back to the global filesystem, and the current local +// path is cleaned for that filesystem. +func WithFilesystem(fs filesystem.FS) SchemaOption { + return func(schema *Schema) *Schema { + if schema == nil { + schema = DefaultSchema() + } + if fs == nil { + fs = filesystem.GetGlobalFileSystem() + } + schema.Filesystem = fs + schema.LocalPath = filesystem.FilePathClean(schema.Filesystem, schema.LocalPath) + return schema + } +} + +// WithFileLimits sets the filesystem read limits used when loading the Schema +// file. +// +// A nil limits value falls back to filesystem.NoLimits(). +func WithFileLimits(limits filesystem.ILimits) SchemaOption { + return func(schema *Schema) *Schema { + if schema == nil { + schema = DefaultSchema() + } + if limits == nil { + limits = filesystem.NoLimits() + } + schema.Limits = limits + return schema + } +} diff --git a/utils/validation/jsonschema/testdata/compound_count.schema.json b/utils/validation/jsonschema/testdata/compound_count.schema.json new file mode 100644 index 0000000000..38b9f3eca8 --- /dev/null +++ b/utils/validation/jsonschema/testdata/compound_count.schema.json @@ -0,0 +1,5 @@ +{ + "$id": "https://example.com/schemas/compound/count.schema.json", + "type": "integer", + "minimum": 0 +} diff --git a/utils/validation/jsonschema/testdata/compound_name.schema.json b/utils/validation/jsonschema/testdata/compound_name.schema.json new file mode 100644 index 0000000000..892dca6561 --- /dev/null +++ b/utils/validation/jsonschema/testdata/compound_name.schema.json @@ -0,0 +1,5 @@ +{ + "$id": "https://example.com/schemas/compound/name.schema.json", + "type": "string", + "minLength": 1 +} diff --git a/utils/validation/jsonschema/testdata/compound_root.schema.json b/utils/validation/jsonschema/testdata/compound_root.schema.json new file mode 100644 index 0000000000..9fcff73980 --- /dev/null +++ b/utils/validation/jsonschema/testdata/compound_root.schema.json @@ -0,0 +1,17 @@ +{ + "$id": "https://example.com/schemas/compound/root.schema.json", + "type": "object", + "properties": { + "name": { + "$ref": "https://example.com/schemas/compound/name.schema.json" + }, + "count": { + "$ref": "https://example.com/schemas/compound/count.schema.json" + } + }, + "required": [ + "name", + "count" + ], + "additionalProperties": false +} diff --git a/utils/validation/jsonschema/testdata/does_not_match.json b/utils/validation/jsonschema/testdata/does_not_match.json new file mode 100644 index 0000000000..cc2560456c --- /dev/null +++ b/utils/validation/jsonschema/testdata/does_not_match.json @@ -0,0 +1,3 @@ +{ + "count": "two" +} diff --git a/utils/validation/jsonschema/testdata/does_not_match.yaml b/utils/validation/jsonschema/testdata/does_not_match.yaml new file mode 100644 index 0000000000..9aa2da9887 --- /dev/null +++ b/utils/validation/jsonschema/testdata/does_not_match.yaml @@ -0,0 +1 @@ +count: two diff --git a/utils/validation/jsonschema/testdata/gojsonschema_fragment_schema.json b/utils/validation/jsonschema/testdata/gojsonschema_fragment_schema.json new file mode 100644 index 0000000000..a7e2f3e5e0 --- /dev/null +++ b/utils/validation/jsonschema/testdata/gojsonschema_fragment_schema.json @@ -0,0 +1,9 @@ +{ + "type": "object", + "additionalProperties": false, + "definitions": { + "x": { + "type": "integer" + } + } +} diff --git a/utils/validation/jsonschema/testdata/gojsonschema_integer.schema.json b/utils/validation/jsonschema/testdata/gojsonschema_integer.schema.json new file mode 100644 index 0000000000..782f200306 --- /dev/null +++ b/utils/validation/jsonschema/testdata/gojsonschema_integer.schema.json @@ -0,0 +1,3 @@ +{ + "type": "integer" +} diff --git a/utils/validation/jsonschema/testdata/gojsonschema_subSchemas.json b/utils/validation/jsonschema/testdata/gojsonschema_subSchemas.json new file mode 100644 index 0000000000..f5577e771a --- /dev/null +++ b/utils/validation/jsonschema/testdata/gojsonschema_subSchemas.json @@ -0,0 +1,8 @@ +{ + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/integer" + } +} diff --git a/utils/validation/jsonschema/testdata/google_defs_invalid.json b/utils/validation/jsonschema/testdata/google_defs_invalid.json new file mode 100644 index 0000000000..6443b729d3 --- /dev/null +++ b/utils/validation/jsonschema/testdata/google_defs_invalid.json @@ -0,0 +1,7 @@ +{ + "$defs": { + "foo": { + "type": 1 + } + } +} diff --git a/utils/validation/jsonschema/testdata/google_defs_metaschema.schema.json b/utils/validation/jsonschema/testdata/google_defs_metaschema.schema.json new file mode 100644 index 0000000000..98f8506f8d --- /dev/null +++ b/utils/validation/jsonschema/testdata/google_defs_metaschema.schema.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/utils/validation/jsonschema/testdata/google_defs_valid.json b/utils/validation/jsonschema/testdata/google_defs_valid.json new file mode 100644 index 0000000000..e8adcf29b7 --- /dev/null +++ b/utils/validation/jsonschema/testdata/google_defs_valid.json @@ -0,0 +1,7 @@ +{ + "$defs": { + "foo": { + "type": "integer" + } + } +} diff --git a/utils/validation/jsonschema/testdata/invalid.json b/utils/validation/jsonschema/testdata/invalid.json new file mode 100644 index 0000000000..f830e8b60c --- /dev/null +++ b/utils/validation/jsonschema/testdata/invalid.json @@ -0,0 +1,3 @@ +{ + "name": +} diff --git a/utils/validation/jsonschema/testdata/json_schema_test_suite_boolean_false.schema.json b/utils/validation/jsonschema/testdata/json_schema_test_suite_boolean_false.schema.json new file mode 100644 index 0000000000..c508d5366f --- /dev/null +++ b/utils/validation/jsonschema/testdata/json_schema_test_suite_boolean_false.schema.json @@ -0,0 +1 @@ +false diff --git a/utils/validation/jsonschema/testdata/json_schema_test_suite_required.schema.json b/utils/validation/jsonschema/testdata/json_schema_test_suite_required.schema.json new file mode 100644 index 0000000000..6e33cf1553 --- /dev/null +++ b/utils/validation/jsonschema/testdata/json_schema_test_suite_required.schema.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": {}, + "bar": {} + }, + "required": [ + "foo" + ] +} diff --git a/utils/validation/jsonschema/testdata/json_schema_test_suite_required_invalid.json b/utils/validation/jsonschema/testdata/json_schema_test_suite_required_invalid.json new file mode 100644 index 0000000000..80ee379a06 --- /dev/null +++ b/utils/validation/jsonschema/testdata/json_schema_test_suite_required_invalid.json @@ -0,0 +1,3 @@ +{ + "bar": 1 +} diff --git a/utils/validation/jsonschema/testdata/json_schema_test_suite_required_valid.json b/utils/validation/jsonschema/testdata/json_schema_test_suite_required_valid.json new file mode 100644 index 0000000000..49761f2a25 --- /dev/null +++ b/utils/validation/jsonschema/testdata/json_schema_test_suite_required_valid.json @@ -0,0 +1,3 @@ +{ + "foo": 1 +} diff --git a/utils/validation/jsonschema/testdata/person.schema.json b/utils/validation/jsonschema/testdata/person.schema.json new file mode 100644 index 0000000000..09b19ae503 --- /dev/null +++ b/utils/validation/jsonschema/testdata/person.schema.json @@ -0,0 +1,16 @@ +{ + "$id": "https://example.com/schemas/person.schema.json", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "count": { + "type": "integer" + } + }, + "required": [ + "name" + ], + "additionalProperties": false +} diff --git a/utils/validation/jsonschema/testdata/valid.json b/utils/validation/jsonschema/testdata/valid.json new file mode 100644 index 0000000000..825c94cadd --- /dev/null +++ b/utils/validation/jsonschema/testdata/valid.json @@ -0,0 +1,4 @@ +{ + "name": "Alice", + "count": 2 +} diff --git a/utils/validation/jsonschema/testdata/valid.yaml b/utils/validation/jsonschema/testdata/valid.yaml new file mode 100644 index 0000000000..cc604f90a7 --- /dev/null +++ b/utils/validation/jsonschema/testdata/valid.yaml @@ -0,0 +1,2 @@ +name: Alice +count: 2 diff --git a/utils/validation/jsonschema/validation.go b/utils/validation/jsonschema/validation.go new file mode 100644 index 0000000000..a6d2b66804 --- /dev/null +++ b/utils/validation/jsonschema/validation.go @@ -0,0 +1,338 @@ +// Package jsonschema provides helpers for validating JSON and YAML content +// against JSON Schema documents. +// +// The package wraps github.com/santhosh-tekuri/jsonschema/v6 and standardises +// a few behaviours used across this repository: +// - schema definitions can be described as files and loaded from either the +// normal filesystem or an embedded filesystem +// - raw JSON values, raw YAML bytes, JSON files, and YAML files can all be +// validated through the same schema abstraction +// - validation and loading failures are wrapped with the repository's common +// error types so callers can test and report them consistently +// - when a schema is composed of multiple schema documents, callers may pass +// an optional schema ID to select the root schema to compile; otherwise it +// should be left as nil +// +// In normal use, callers describe one or more schema files with Schema values, +// optionally pass a schema ID when the schema set is composed of multiple +// documents, and then validate either raw content or files through the helper +// functions in this package. +package jsonschema + +import ( + "context" + "slices" + "strings" + + validation "github.com/go-ozzo/ozzo-validation/v4" + santhoshjsonschema "github.com/santhosh-tekuri/jsonschema/v6" + + "github.com/ARM-software/golang-utils/utils/collection" + "github.com/ARM-software/golang-utils/utils/commonerrors" + "github.com/ARM-software/golang-utils/utils/config" + "github.com/ARM-software/golang-utils/utils/field" + "github.com/ARM-software/golang-utils/utils/filesystem" + "github.com/ARM-software/golang-utils/utils/reflection" + "github.com/ARM-software/golang-utils/utils/serialization/json" //nolint:misspell +) + +// Schema describes a schema file that can be loaded and registered for +// validation. +// +// It is intended to be a lightweight wrapper around a schema file so callers +// can build validators from normal or embedded filesystems without passing raw +// schema bytes around manually. +type Schema struct { + // Title is a human-readable title of the schema. + Title string + // LocalPath is the path to the schema file. It should be available on the + // filesystem or an embedded filesystem. + LocalPath string + // ID should match the $id field within the schema. + ID string + // Filesystem is the filesystem to use to load the schema file. + Filesystem filesystem.FS + + // Limits defines the filesystem read limits used when loading the schema + // file. + Limits filesystem.ILimits +} + +// Validate checks that the schema definition contains the required fields +// needed to load a schema file. +func (s *Schema) Validate() error { + if s == nil { + return commonerrors.UndefinedVariable("schema") + } + err := config.ValidateEmbedded(s) + if err == nil { + err = validation.ValidateStruct(s, + validation.Field(&s.Title, validation.Required), + validation.Field(&s.LocalPath, validation.Required), + validation.Field(&s.ID, validation.Required), + validation.Field(&s.Filesystem, validation.Required), + validation.Field(&s.Limits, validation.Required), + ) + } + + if err != nil { + return commonerrors.WrapError(commonerrors.ErrInvalid, err, "invalid json schema definition") + } + return nil +} + +// SchemaSpec contains the raw JSON schema document and the identifier used to +// register it with the JSON Schema compiler. +type SchemaSpec struct { + ID string + Specification []byte + // Title is a human-readable title of the schema. + Title string +} + +// Validate checks that the schema specification contains the required fields +// needed to register and compile a schema. +func (s *SchemaSpec) Validate() error { + if s == nil { + return commonerrors.UndefinedVariable("schema specification") + } + err := validation.ValidateStruct(s, + validation.Field(&s.ID, validation.Required), + validation.Field(&s.Specification, validation.Required), + ) + if err != nil { + return commonerrors.WrapError(commonerrors.ErrInvalid, err, "invalid json schema specification") + } + return nil +} + +// LoadSchemaSpec reads and returns the raw schema specification described by +// schema. +// +// The schema file is read from the filesystem attached to the Schema, which may +// be a normal or embedded filesystem. +func LoadSchemaSpec(ctx context.Context, schema *Schema) (*SchemaSpec, error) { + if schema == nil { + return nil, commonerrors.UndefinedVariable("schema") + } + schema = schema.Default() + err := schema.Validate() + if err != nil { + return nil, err + } + + data, err := schema.Filesystem.ReadFileWithContextAndLimits(ctx, filesystem.FilePathClean(schema.Filesystem, schema.LocalPath), schema.Limits) + if err != nil { + return nil, commonerrors.DescribeCircumstancef(err, "failed to load JSON Schema [%v] from [%v]", schema.Title, schema.LocalPath) + } + + return &SchemaSpec{ + ID: schemaID(schema.ID, schema.LocalPath), + Specification: data, + Title: schema.Title, + }, nil +} + +// ValidateRawJSONAgainstSchema validates JSON-compatible content against the +// supplied schema definitions. +// +// `schemaID` is the optional schema ID to compile when the schema is composed +// of multiple schema documents. Otherwise, leave it as nil. +// +// The content may already be decoded into Go values, or any other value that +// can be passed directly to the underlying JSON Schema validator. +func ValidateRawJSONAgainstSchema(ctx context.Context, content any, schemaID *string, schema ...Schema) error { + v, err := NewJSONFileValidator(schemaID, schema...) + if err != nil { + return err + } + return v.ValidateContent(ctx, content) +} + +// ValidateRawJSONAgainstSchemaOptions validates JSON-compatible content against +// a Schema built from the supplied options. +func ValidateRawJSONAgainstSchemaOptions(ctx context.Context, content any, options ...SchemaOption) error { + return ValidateRawJSONAgainstSchema(ctx, content, nil, *NewJSONSchemaFile(options...)) +} + +// ValidateRawYAMLAgainstSchema validates raw YAML bytes against the supplied +// schema definitions. +// +// `schemaID` is the optional schema ID to compile when the schema is composed +// of multiple schema documents. Otherwise, leave it as nil. +// +// The YAML content is converted to JSON first and then validated against the +// compiled JSON Schema. +func ValidateRawYAMLAgainstSchema(ctx context.Context, content []byte, schemaID *string, schema ...Schema) error { + v, err := NewYAMLFileValidator(schemaID, schema...) + if err != nil { + return err + } + return v.ValidateContent(ctx, content) +} + +// ValidateRawYAMLAgainstSchemaOptions validates raw YAML bytes against a Schema +// built from the supplied options. +func ValidateRawYAMLAgainstSchemaOptions(ctx context.Context, content []byte, options ...SchemaOption) error { + return ValidateRawYAMLAgainstSchema(ctx, content, nil, *NewJSONSchemaFile(options...)) +} + +// ValidateJSONFileAgainstSchemaFS validates a JSON file from the supplied +// filesystem against the supplied schema definitions. +// +// `schemaID` is the optional schema ID to compile when the schema is composed +// of multiple schema documents. Otherwise, leave it as nil. +func ValidateJSONFileAgainstSchemaFS(ctx context.Context, fileSystem filesystem.FS, filePath string, schemaID *string, schema ...Schema) error { + v, err := NewJSONFileValidator(schemaID, schema...) + if err != nil { + return err + } + return v.ValidateFileInFS(ctx, fileSystem, filePath) +} + +// ValidateJSONFileAgainstSchemaFSWithOptions validates a JSON file from the +// supplied filesystem against a Schema built from the supplied options. +func ValidateJSONFileAgainstSchemaFSWithOptions(ctx context.Context, fileSystem filesystem.FS, filePath string, options ...SchemaOption) error { + return ValidateJSONFileAgainstSchemaFS(ctx, fileSystem, filePath, nil, *NewJSONSchemaFile(options...)) +} + +// ValidateYAMLFileAgainstSchemaFS validates a YAML file from the supplied +// filesystem against the supplied schema definitions. +// +// `schemaID` is the optional schema ID to compile when the schema is composed +// of multiple schema documents. Otherwise, leave it as nil. +func ValidateYAMLFileAgainstSchemaFS(ctx context.Context, fileSystem filesystem.FS, filePath string, schemaID *string, schema ...Schema) error { + v, err := NewYAMLFileValidator(schemaID, schema...) + if err != nil { + return err + } + return v.ValidateFileInFS(ctx, fileSystem, filePath) +} + +// ValidateYAMLFileAgainstSchemaFSWithOptions validates a YAML file from the +// supplied filesystem against a Schema built from the supplied options. +func ValidateYAMLFileAgainstSchemaFSWithOptions(ctx context.Context, fileSystem filesystem.FS, filePath string, options ...SchemaOption) error { + return ValidateYAMLFileAgainstSchemaFS(ctx, fileSystem, filePath, nil, *NewJSONSchemaFile(options...)) +} + +// ValidateJSONFileAgainstSchema validates a JSON file from the global +// filesystem against the supplied schema definitions. +// +// `schemaID` is the optional schema ID to compile when the schema is composed +// of multiple schema documents. Otherwise, leave it as nil. +func ValidateJSONFileAgainstSchema(ctx context.Context, filePath string, schemaID *string, schema ...Schema) error { + v, err := NewJSONFileValidator(schemaID, schema...) + if err != nil { + return err + } + return v.ValidateFile(ctx, filePath) +} + +// ValidateJSONFileAgainstSchemaOptions validates a JSON file against a Schema +// built from the supplied options. +func ValidateJSONFileAgainstSchemaOptions(ctx context.Context, filePath string, options ...SchemaOption) error { + return ValidateJSONFileAgainstSchema(ctx, filePath, nil, *NewJSONSchemaFile(options...)) +} + +// ValidateYAMLFileAgainstSchema validates a YAML file from the global +// filesystem against the supplied schema definitions. +// +// `schemaID` is the optional schema ID to compile when the schema is composed +// of multiple schema documents. Otherwise, leave it as nil. +func ValidateYAMLFileAgainstSchema(ctx context.Context, filePath string, schemaID *string, schema ...Schema) error { + v, err := NewYAMLFileValidator(schemaID, schema...) + if err != nil { + return err + } + return v.ValidateFile(ctx, filePath) +} + +// ValidateYAMLFileAgainstSchemaOptions validates a YAML file against a Schema +// built from the supplied options. +func ValidateYAMLFileAgainstSchemaOptions(ctx context.Context, filePath string, options ...SchemaOption) error { + return ValidateYAMLFileAgainstSchema(ctx, filePath, nil, *NewJSONSchemaFile(options...)) +} + +// validateAgainstSchema validates a file against a schema +func validateAgainstSchema(schema *santhoshjsonschema.Schema, content any) (err error) { + if reflection.IsEmpty(content) { + err = commonerrors.UndefinedVariable("content to validate against schema") + return + } + if reflection.IsEmpty(schema) { + err = commonerrors.UndefinedVariable("schema to validate against") + return + } + err = schema.Validate(content) + if err != nil { + err = commonerrors.WrapError(commonerrors.ErrInvalid, err, "content does not comply with JSON schema") + return + } + return +} + +// generateSchemaDefinition compiles the schema set used for validation. +// +// `schemaID` is the optional schema ID to compile when the schema is composed +// of multiple schema documents. Otherwise, leave it as nil. +func generateSchemaDefinition(ctx context.Context, schemaID *string, composingSchemas ...Schema) (schema *santhoshjsonschema.Schema, err error) { + if len(composingSchemas) == 0 { + err = commonerrors.UndefinedVariable("schemas") + return + } + id := field.OptionalString(schemaID, composingSchemas[0].ID) + + compiler := santhoshjsonschema.NewCompiler() + err = collection.EachRef[Schema](slices.Values(composingSchemas), func(schema *Schema) error { + spec, subErr := LoadSchemaSpec(ctx, schema) + if subErr != nil { + return subErr + } + return registerSchemaToCompiler(ctx, compiler, spec) + }) + if err != nil { + return nil, err + } + + schema, err = compiler.Compile(id) + if err != nil { + err = commonerrors.WrapErrorf(commonerrors.ErrUnexpected, err, "failed to compile schema [%v]", id) + } + return +} + +func registerSchemaToCompiler(ctx context.Context, compiler *santhoshjsonschema.Compiler, spec *SchemaSpec) (err error) { + if compiler == nil { + err = commonerrors.UndefinedVariable("json schema compiler") + return + } + if spec == nil { + err = commonerrors.UndefinedVariable("schema specification") + return + } + err = spec.Validate() + if err != nil { + return + } + + var doc any + err = json.UnmarshallWithContext(ctx, spec.Specification, &doc) + if err != nil { + err = commonerrors.WrapErrorf(commonerrors.ErrMarshalling, err, "failed to decode JSON Schema specification [%v]", spec.Title) + return + } + + err = compiler.AddResource(spec.ID, doc) + if err != nil { + err = commonerrors.WrapErrorf(commonerrors.ErrUnexpected, err, "failed to register schema [%v] to schema compiler", spec.Title) + } + return +} + +func schemaID(id string, localPath string) string { + id = strings.TrimSpace(id) + if !reflection.IsEmpty(id) { + return id + } + return strings.TrimSpace(localPath) +} diff --git a/utils/validation/jsonschema/validation_test.go b/utils/validation/jsonschema/validation_test.go new file mode 100644 index 0000000000..6e2c3922b7 --- /dev/null +++ b/utils/validation/jsonschema/validation_test.go @@ -0,0 +1,510 @@ +package jsonschema + +import ( + "context" + "embed" + "path" + "testing" + + santhoshjsonschema "github.com/santhosh-tekuri/jsonschema/v6" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ARM-software/golang-utils/utils/commonerrors" + "github.com/ARM-software/golang-utils/utils/commonerrors/errortest" + "github.com/ARM-software/golang-utils/utils/filesystem" +) + +const compoundRootSchemaID = "https://example.com/schemas/compound/root.schema.json" + +var ( + //go:embed testdata/*.json testdata/*.yaml + embeddedFS embed.FS +) + +func newValidSchema(t *testing.T, fs filesystem.FS) *Schema { + t.Helper() + return NewJSONSchemaFile( + WithTitle("person"), + WithLocalPath(path.Join("testdata", "person.schema.json")), + WithID("https://example.com/schemas/person.schema.json"), + WithFilesystem(fs), + ) +} + +func newSchema(t *testing.T, fs filesystem.FS, title string, localPath string, id string) *Schema { + t.Helper() + return NewJSONSchemaFile(WithTitle(title), WithLocalPath(localPath), WithID(id), WithFilesystem(fs)) +} + +func newMissingSchema(t *testing.T, fs filesystem.FS) *Schema { + t.Helper() + return newSchema( + t, + fs, + "missing schema", + path.Join("testdata", "missing.schema.json"), + "https://example.com/schemas/missing.schema.json", + ) +} + +func newEmbeddedFilesystem(t *testing.T) filesystem.FS { + t.Helper() + fs, err := filesystem.NewEmbedFileSystem(&embeddedFS) + require.NoError(t, err) + return fs +} + +func newCompoundSchemas(t *testing.T, fs filesystem.FS) []Schema { + t.Helper() + return []Schema{ + { + Title: "compound name", + LocalPath: path.Join("testdata", "compound_name.schema.json"), + ID: "https://example.com/schemas/compound/name.schema.json", + Filesystem: fs, + }, + { + Title: "compound count", + LocalPath: path.Join("testdata", "compound_count.schema.json"), + ID: "https://example.com/schemas/compound/count.schema.json", + Filesystem: fs, + }, + { + Title: "compound root", + LocalPath: path.Join("testdata", "compound_root.schema.json"), + ID: compoundRootSchemaID, + Filesystem: fs, + }, + } +} + +func TestSchemaValidate(t *testing.T) { + require.NoError(t, newValidSchema(t, filesystem.GetGlobalFileSystem()).Validate()) + + var nilSchema *Schema + err := nilSchema.Validate() + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrUndefined) + + err = (&Schema{}).Validate() + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrInvalid) +} + +func TestNewJSONSchemaFileDefaults(t *testing.T) { + schema := NewJSONSchemaFile(WithTitle("person"), WithLocalPath(path.Join("testdata", "person.schema.json"))) + require.NotNil(t, schema) + assert.Equal(t, "person", schema.Title) + assert.Equal(t, path.Join("testdata", "person.schema.json"), schema.LocalPath) + assert.Equal(t, path.Join("testdata", "person.schema.json"), schema.ID) + assert.Equal(t, filesystem.GetGlobalFileSystem(), schema.Filesystem) +} + +func TestNewJSONSchemaFile(t *testing.T) { + schema := NewJSONSchemaFile( + WithTitle("person"), + WithLocalPath(path.Join("testdata", "person.schema.json")), + WithFilesystem(filesystem.GetGlobalFileSystem()), + ) + require.NotNil(t, schema) + assert.Equal(t, "person", schema.Title) + assert.Equal(t, path.Join("testdata", "person.schema.json"), schema.ID) +} + +func TestWithFileLimits(t *testing.T) { + limits := filesystem.DefaultLimits() + schema := NewJSONSchemaFile( + WithTitle("person"), + WithLocalPath(path.Join("testdata", "person.schema.json")), + WithFileLimits(limits), + ) + require.NotNil(t, schema) + assert.Equal(t, limits, schema.Limits) +} + +func TestSchemaSpecValidate(t *testing.T) { + require.NoError(t, (&SchemaSpec{ID: "schema.json", Specification: []byte(`{"type":"object"}`)}).Validate()) + + var nilSpec *SchemaSpec + err := nilSpec.Validate() + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrUndefined) + + err = (&SchemaSpec{}).Validate() + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrInvalid) +} + +func TestLoadSchemaSpec(t *testing.T) { + spec, err := LoadSchemaSpec(context.Background(), newValidSchema(t, filesystem.GetGlobalFileSystem())) + require.NoError(t, err) + assert.Equal(t, "https://example.com/schemas/person.schema.json", spec.ID) + assert.NotEmpty(t, spec.Specification) +} + +func TestLoadSchemaSpecEmbeddedFS(t *testing.T) { + spec, err := LoadSchemaSpec(context.Background(), newValidSchema(t, newEmbeddedFilesystem(t))) + require.NoError(t, err) + assert.Equal(t, "https://example.com/schemas/person.schema.json", spec.ID) + assert.NotEmpty(t, spec.Specification) +} + +func TestLoadSchemaSpec_MissingSchemaPath(t *testing.T) { + _, err := LoadSchemaSpec(context.Background(), newMissingSchema(t, filesystem.GetGlobalFileSystem())) + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrNotFound) + assert.ErrorContains(t, err, "failed to load JSON Schema") +} + +func TestValidateRawJSONAgainstSchema(t *testing.T) { + err := ValidateRawJSONAgainstSchema(context.Background(), map[string]any{"name": "Alice", "count": 2}, nil, *newValidSchema(t, filesystem.GetGlobalFileSystem())) + require.NoError(t, err) +} + +func TestValidateRawJSONAgainstSchemaOptions(t *testing.T) { + err := ValidateRawJSONAgainstSchemaOptions( + context.Background(), + map[string]any{"name": "Alice", "count": 2}, + nil, + WithTitle("person"), + WithLocalPath(path.Join("testdata", "person.schema.json")), + WithFilesystem(filesystem.GetGlobalFileSystem()), + ) + require.NoError(t, err) +} + +func TestValidateRawJSONAgainstSchema_InvalidContent(t *testing.T) { + err := ValidateRawJSONAgainstSchema(context.Background(), map[string]any{"count": "two"}, nil, *newValidSchema(t, filesystem.GetGlobalFileSystem())) + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrInvalid) +} + +func TestValidateRawJSONAgainstCompoundSchema(t *testing.T) { + schemaID := compoundRootSchemaID + err := ValidateRawJSONAgainstSchema( + context.Background(), + map[string]any{"name": "Alice", "count": 2}, + &schemaID, + newCompoundSchemas(t, filesystem.GetGlobalFileSystem())..., + ) + require.NoError(t, err) +} + +func TestValidateGoogleDraft202012DefsFixture(t *testing.T) { + // Source: https://github.com/json-schema-org/JSON-Schema-Test-Suite/blob/main/tests/draft2020-12/defs.json + googleSchema := newSchema( + t, + filesystem.GetGlobalFileSystem(), + "google defs metaschema fixture", + path.Join("testdata", "google_defs_metaschema.schema.json"), + "https://example.com/schemas/google/defs-metaschema.schema.json", + ) + + err := ValidateJSONFileAgainstSchema( + context.Background(), + path.Join("testdata", "google_defs_valid.json"), + nil, + *googleSchema, + ) + require.NoError(t, err) + + err = ValidateJSONFileAgainstSchema( + context.Background(), + path.Join("testdata", "google_defs_invalid.json"), + nil, + *googleSchema, + ) + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrInvalid) +} + +func TestValidateGojsonschemaIntegerFixture(t *testing.T) { + // Source: https://github.com/xeipuuv/gojsonschema/blob/master/testdata/remotes/integer.json + gojsonschemaFixture := newSchema( + t, + filesystem.GetGlobalFileSystem(), + "gojsonschema integer fixture", + path.Join("testdata", "gojsonschema_integer.schema.json"), + "https://example.com/schemas/gojsonschema/integer.json", + ) + + err := ValidateRawJSONAgainstSchema(context.Background(), 2, nil, *gojsonschemaFixture) + require.NoError(t, err) + + err = ValidateRawJSONAgainstSchema(context.Background(), "2", nil, *gojsonschemaFixture) + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrInvalid) +} + +func TestValidateJSONSchemaTestSuiteRequiredFixture(t *testing.T) { + // Source: https://github.com/json-schema-org/JSON-Schema-Test-Suite/blob/main/tests/draft2020-12/required.json + requiredFixture := newSchema( + t, + filesystem.GetGlobalFileSystem(), + "json-schema-test-suite required fixture", + path.Join("testdata", "json_schema_test_suite_required.schema.json"), + "https://example.com/schemas/json-schema-test-suite/required.schema.json", + ) + + err := ValidateJSONFileAgainstSchema( + context.Background(), + path.Join("testdata", "json_schema_test_suite_required_valid.json"), + nil, + *requiredFixture, + ) + require.NoError(t, err) + + err = ValidateJSONFileAgainstSchema( + context.Background(), + path.Join("testdata", "json_schema_test_suite_required_invalid.json"), + nil, + *requiredFixture, + ) + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrInvalid) +} + +func TestValidateJSONSchemaTestSuiteBooleanFalseFixture(t *testing.T) { + // Source: https://github.com/json-schema-org/JSON-Schema-Test-Suite/blob/main/tests/draft2020-12/boolean_schema.json + booleanFalseFixture := newSchema( + t, + filesystem.GetGlobalFileSystem(), + "json-schema-test-suite boolean false fixture", + path.Join("testdata", "json_schema_test_suite_boolean_false.schema.json"), + "https://example.com/schemas/json-schema-test-suite/boolean-false.schema.json", + ) + + err := ValidateRawJSONAgainstSchema(context.Background(), 1, nil, *booleanFalseFixture) + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrInvalid) + + err = ValidateRawJSONAgainstSchema(context.Background(), map[string]any{"foo": "bar"}, nil, *booleanFalseFixture) + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrInvalid) +} + +func TestValidateGojsonschemaFragmentSchemaFixture(t *testing.T) { + // Source: https://github.com/xeipuuv/gojsonschema/blob/master/testdata/extra/fragment_schema.json + fragmentFixture := newSchema( + t, + filesystem.GetGlobalFileSystem(), + "gojsonschema fragment schema fixture", + path.Join("testdata", "gojsonschema_fragment_schema.json"), + "https://example.com/schemas/gojsonschema/fragment_schema.json", + ) + + schemaID := "https://example.com/schemas/gojsonschema/fragment_schema.json#/definitions/x" + err := ValidateRawJSONAgainstSchema(context.Background(), 2, &schemaID, *fragmentFixture) + require.NoError(t, err) + + err = ValidateRawJSONAgainstSchema(context.Background(), "2", &schemaID, *fragmentFixture) + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrInvalid) +} + +func TestValidateGojsonschemaSubSchemasFixture(t *testing.T) { + // Source: https://github.com/xeipuuv/gojsonschema/blob/master/testdata/remotes/subSchemas.json + subSchemasFixture := newSchema( + t, + filesystem.GetGlobalFileSystem(), + "gojsonschema subSchemas fixture", + path.Join("testdata", "gojsonschema_subSchemas.json"), + "https://example.com/schemas/gojsonschema/subSchemas.json", + ) + + schemaID := "https://example.com/schemas/gojsonschema/subSchemas.json#/refToInteger" + err := ValidateRawJSONAgainstSchema(context.Background(), 2, &schemaID, *subSchemasFixture) + require.NoError(t, err) + + err = ValidateRawJSONAgainstSchema(context.Background(), "2", &schemaID, *subSchemasFixture) + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrInvalid) +} + +func TestValidateRawYAMLAgainstSchema(t *testing.T) { + err := ValidateRawYAMLAgainstSchema(context.Background(), []byte("name: Alice\ncount: 2\n"), nil, *newValidSchema(t, filesystem.GetGlobalFileSystem())) + require.NoError(t, err) +} + +func TestValidateRawYAMLAgainstSchemaOptions(t *testing.T) { + err := ValidateRawYAMLAgainstSchemaOptions( + context.Background(), + []byte("name: Alice\ncount: 2\n"), + nil, + WithTitle("person"), + WithLocalPath(path.Join("testdata", "person.schema.json")), + WithFilesystem(filesystem.GetGlobalFileSystem()), + ) + require.NoError(t, err) +} + +func TestValidateRawYAMLAgainstSchema_InvalidContent(t *testing.T) { + err := ValidateRawYAMLAgainstSchema(context.Background(), []byte("count: two\n"), nil, *newValidSchema(t, filesystem.GetGlobalFileSystem())) + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrInvalid) +} + +func TestRegisterSchemaToCompiler_InvalidSchemaSpec(t *testing.T) { + err := registerSchemaToCompiler(context.Background(), santhoshjsonschema.NewCompiler(), &SchemaSpec{}) + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrInvalid) +} + +func TestRegisterSchemaToCompiler_InvalidSpecificationJSON(t *testing.T) { + err := registerSchemaToCompiler(context.Background(), santhoshjsonschema.NewCompiler(), &SchemaSpec{ + ID: "schema.json", + Title: "broken", + Specification: []byte(`{"type":`), + }) + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrMarshalling) +} + +func TestGenerateSchemaDefinition(t *testing.T) { + sch, err := generateSchemaDefinition(context.Background(), nil, *newValidSchema(t, filesystem.GetGlobalFileSystem())) + require.NoError(t, err) + assert.NotNil(t, sch) +} + +func TestSchemaGenerationOnlyRunsOnceAfterCancelledContext(t *testing.T) { + v, err := NewJSONFileValidator(nil, *newValidSchema(t, filesystem.GetGlobalFileSystem())) + require.NoError(t, err) + + cancelledCtx, cancel := context.WithCancel(context.Background()) + cancel() + + require.NoError(t, v.ValidateContent(cancelledCtx, map[string]any{"name": "Alice", "count": 2})) + require.NoError(t, v.ValidateContent(context.Background(), map[string]any{"name": "Alice", "count": 2})) +} + +func TestNewJSONFileValidatorWithOptions(t *testing.T) { + v, err := NewJSONFileValidatorWithOptions( + nil, + WithTitle("person"), + WithLocalPath(path.Join("testdata", "person.schema.json")), + WithFilesystem(filesystem.GetGlobalFileSystem()), + ) + require.NoError(t, err) + require.NoError(t, v.ValidateContent(context.Background(), map[string]any{"name": "Alice", "count": 2})) +} + +func TestNewYAMLFileValidatorWithOptions(t *testing.T) { + v, err := NewYAMLFileValidatorWithOptions( + nil, + WithTitle("person"), + WithLocalPath(path.Join("testdata", "person.schema.json")), + WithFilesystem(filesystem.GetGlobalFileSystem()), + ) + require.NoError(t, err) + require.NoError(t, v.ValidateContent(context.Background(), []byte("name: Alice\ncount: 2\n"))) +} + +func TestNewJSONFileValidatorWithOptions_LowSchemaFileLimit(t *testing.T) { + v, err := NewJSONFileValidatorWithOptions( + WithTitle("person"), + WithLocalPath(path.Join("testdata", "person.schema.json")), + WithFilesystem(filesystem.GetGlobalFileSystem()), + WithFileLimits(filesystem.NewLimits(1, 1024, 1, 1, false)), + ) + require.NoError(t, err) + + err = v.ValidateContent(context.Background(), map[string]any{"name": "Alice", "count": 2}) + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrTooLarge) +} + +func TestValidateJSONFileAgainstSchema(t *testing.T) { + err := ValidateJSONFileAgainstSchema(context.Background(), path.Join("testdata", "valid.json"), nil, *newValidSchema(t, filesystem.GetGlobalFileSystem())) + require.NoError(t, err) +} + +func TestValidateJSONFileAgainstSchemaOptions(t *testing.T) { + err := ValidateJSONFileAgainstSchemaOptions( + context.Background(), + path.Join("testdata", "valid.json"), + nil, + WithTitle("person"), + WithLocalPath(path.Join("testdata", "person.schema.json")), + WithFilesystem(filesystem.GetGlobalFileSystem()), + ) + require.NoError(t, err) +} + +func TestValidateJSONFileAgainstSchema_MissingSchemaPath(t *testing.T) { + err := ValidateJSONFileAgainstSchema(context.Background(), path.Join("testdata", "valid.json"), nil, *newMissingSchema(t, filesystem.GetGlobalFileSystem())) + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrNotFound) + assert.ErrorContains(t, err, "failed to load JSON Schema") +} + +func TestValidateJSONFileAgainstSchemaFS(t *testing.T) { + err := ValidateJSONFileAgainstSchemaFS(context.Background(), newEmbeddedFilesystem(t), path.Join("testdata", "valid.json"), nil, *newValidSchema(t, newEmbeddedFilesystem(t))) + require.NoError(t, err) +} + +func TestValidateJSONFileAgainstSchemaFSWithOptions(t *testing.T) { + err := ValidateJSONFileAgainstSchemaFSWithOptions( + context.Background(), + newEmbeddedFilesystem(t), + path.Join("testdata", "valid.json"), + nil, + WithTitle("person"), + WithLocalPath(path.Join("testdata", "person.schema.json")), + WithFilesystem(newEmbeddedFilesystem(t)), + ) + require.NoError(t, err) +} + +func TestValidateYAMLFileAgainstSchema(t *testing.T) { + err := ValidateYAMLFileAgainstSchema(context.Background(), path.Join("testdata", "valid.yaml"), nil, *newValidSchema(t, filesystem.GetGlobalFileSystem())) + require.NoError(t, err) +} + +func TestValidateYAMLFileAgainstSchemaOptions(t *testing.T) { + err := ValidateYAMLFileAgainstSchemaOptions( + context.Background(), + path.Join("testdata", "valid.yaml"), + nil, + WithTitle("person"), + WithLocalPath(path.Join("testdata", "person.schema.json")), + WithFilesystem(filesystem.GetGlobalFileSystem()), + ) + require.NoError(t, err) +} + +func TestValidateYAMLFileAgainstSchemaFS(t *testing.T) { + err := ValidateYAMLFileAgainstSchemaFS(context.Background(), newEmbeddedFilesystem(t), path.Join("testdata", "valid.yaml"), nil, *newValidSchema(t, newEmbeddedFilesystem(t))) + require.NoError(t, err) +} + +func TestValidateYAMLFileAgainstSchemaFSWithOptions(t *testing.T) { + err := ValidateYAMLFileAgainstSchemaFSWithOptions( + context.Background(), + newEmbeddedFilesystem(t), + path.Join("testdata", "valid.yaml"), + nil, + WithTitle("person"), + WithLocalPath(path.Join("testdata", "person.schema.json")), + WithFilesystem(newEmbeddedFilesystem(t)), + ) + require.NoError(t, err) +} + +func TestValidateJSONFileAgainstSchema_InvalidJSONFile(t *testing.T) { + err := ValidateJSONFileAgainstSchema(context.Background(), path.Join("testdata", "invalid.json"), nil, *newValidSchema(t, filesystem.GetGlobalFileSystem())) + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrMarshalling) +} + +func TestValidateJSONFileAgainstSchema_InvalidDataFile(t *testing.T) { + err := ValidateJSONFileAgainstSchema(context.Background(), path.Join("testdata", "does_not_match.json"), nil, *newValidSchema(t, filesystem.GetGlobalFileSystem())) + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrInvalid) +} + +func TestValidateYAMLFileAgainstSchema_InvalidDataFile(t *testing.T) { + err := ValidateYAMLFileAgainstSchema(context.Background(), path.Join("testdata", "does_not_match.yaml"), nil, *newValidSchema(t, filesystem.GetGlobalFileSystem())) + require.Error(t, err) + errortest.AssertError(t, err, commonerrors.ErrInvalid) +} diff --git a/utils/validation/jsonschema/validator.go b/utils/validation/jsonschema/validator.go new file mode 100644 index 0000000000..c4810e7c20 --- /dev/null +++ b/utils/validation/jsonschema/validator.go @@ -0,0 +1,138 @@ +//nolint:misspell // serialization package names and aliases are intentional. +package jsonschema + +import ( + "context" + "sync" + + "github.com/santhosh-tekuri/jsonschema/v6" + + "github.com/ARM-software/golang-utils/utils/commonerrors" + "github.com/ARM-software/golang-utils/utils/filesystem" + jsonserialization "github.com/ARM-software/golang-utils/utils/serialization/json" //nolint:misspell + "github.com/ARM-software/golang-utils/utils/serialization/yaml" //nolint:misspell +) + +type fileValidator struct { + schemaCreationFunc func(context.Context) (*jsonschema.Schema, error) + expectedExtensions []string + convertFunc func([]byte) ([]byte, error) +} + +func (c *fileValidator) ValidateFileWithLimits(ctx context.Context, filepath string, fileLimits filesystem.ILimits) error { + return c.ValidateFileInFSWithLimits(ctx, filesystem.GetGlobalFileSystem(), filepath, fileLimits) +} + +func (c *fileValidator) ValidateFileInFSWithLimits(ctx context.Context, fs filesystem.FS, filepath string, fileLimits filesystem.ILimits) (err error) { + if fs == nil { + err = commonerrors.UndefinedVariable("file system") + return + } + err = filesystem.NewPathExtensionRule(fs, true, c.expectedExtensions...).Validate(filepath) + if err != nil { + return + } + err = filesystem.NewPathExistRule(fs, true).Validate(filepath) + if err != nil { + return + } + content, err := fs.ReadFileWithContextAndLimits(ctx, filepath, fileLimits) + if err != nil { + return + } + err = c.ValidateContent(ctx, content) + return +} + +func (c *fileValidator) ValidateContent(ctx context.Context, a any) error { + if bytes, ok := a.([]byte); ok { + if c.convertFunc != nil { + var err error + bytes, err = c.convertFunc(bytes) + if err != nil { + return err + } + } + var decoded any + err := jsonserialization.UnmarshallWithContext(ctx, bytes, &decoded) + if err != nil { + return commonerrors.WrapError(commonerrors.ErrMarshalling, err, "failed to decode JSON content") + } + a = decoded + } + schema, err := c.schemaCreationFunc(ctx) + if err != nil { + return err + } + return validateAgainstSchema(schema, a) +} + +func (c *fileValidator) ValidateFile(ctx context.Context, filepath string) error { + return c.ValidateFileWithLimits(ctx, filepath, filesystem.NoLimits()) +} + +func (c *fileValidator) ValidateFileInFS(ctx context.Context, fs filesystem.FS, filepath string) error { + return c.ValidateFileInFSWithLimits(ctx, fs, filepath, filesystem.NoLimits()) +} + +func newSchemaCreationFunc(schemaID *string, schema ...Schema) func(context.Context) (*jsonschema.Schema, error) { + var once sync.Once + var compiled *jsonschema.Schema + var compiledErr error + + return func(ctx context.Context) (*jsonschema.Schema, error) { + once.Do(func() { + compileCtx := context.Background() + if ctx != nil { + compileCtx = context.WithoutCancel(ctx) + } + compiled, compiledErr = generateSchemaDefinition(compileCtx, schemaID, schema...) + }) + return compiled, compiledErr + } +} + +// NewJSONFileValidator returns a validator for JSON content and `.json` files. +// +// `schemaID` is the optional schema ID to compile when the schema is composed +// of multiple schema documents. Otherwise, leave it as nil. +// +// The returned validator compiles its schema set lazily and caches the result +// for reuse across subsequent validations. +func NewJSONFileValidator(schemaID *string, schema ...Schema) (v ISchemaValidator, err error) { + v = &fileValidator{ + schemaCreationFunc: newSchemaCreationFunc(schemaID, schema...), + expectedExtensions: []string{".json"}, + convertFunc: nil, + } + return +} + +// NewJSONFileValidatorWithOptions returns a JSON validator using a Schema built +// from the supplied options. +func NewJSONFileValidatorWithOptions(options ...SchemaOption) (ISchemaValidator, error) { + return NewJSONFileValidator(nil, *NewJSONSchemaFile(options...)) +} + +// NewYAMLFileValidator returns a validator for YAML content and `.yaml` or +// `.yml` files. +// +// `schemaID` is the optional schema ID to compile when the schema is composed +// of multiple schema documents. Otherwise, leave it as nil. +// +// The returned validator compiles its schema set lazily and caches the result +// for reuse across subsequent validations. +func NewYAMLFileValidator(schemaID *string, schema ...Schema) (v ISchemaValidator, err error) { + v = &fileValidator{ + schemaCreationFunc: newSchemaCreationFunc(schemaID, schema...), + expectedExtensions: []string{".yaml", ".yml"}, + convertFunc: yaml.ToJSON, + } + return +} + +// NewYAMLFileValidatorWithOptions returns a YAML validator using a Schema built +// from the supplied options. +func NewYAMLFileValidatorWithOptions(options ...SchemaOption) (ISchemaValidator, error) { + return NewYAMLFileValidator(nil, *NewJSONSchemaFile(options...)) +} diff --git a/utils/value/empty.go b/utils/value/empty.go index 13162c61b2..aab80f6128 100644 --- a/utils/value/empty.go +++ b/utils/value/empty.go @@ -3,6 +3,7 @@ package value import ( "reflect" "strings" + "unsafe" ) // IsEmpty checks whether a value is empty i.e. "", nil, 0, [], {}, false, etc. @@ -33,9 +34,18 @@ func IsEmpty(value any) bool { return true } deref := objValue.Elem().Interface() + if IsNilInterface(deref) { + return true + } return IsEmpty(deref) default: zero := reflect.Zero(objValue.Type()) return reflect.DeepEqual(value, zero.Interface()) } } + +// IsNilInterface checks whether an interface value is nil even when it has been +// passed around as `any`. +func IsNilInterface(i any) bool { + return (*[2]uintptr)(unsafe.Pointer(&i))[1] == 0 +} diff --git a/utils/value/empty_test.go b/utils/value/empty_test.go index b01224d458..3fbb85a99b 100644 --- a/utils/value/empty_test.go +++ b/utils/value/empty_test.go @@ -134,3 +134,15 @@ func TestIsEmpty(t *testing.T) { }) } } + +func TestIsNilInterface(t *testing.T) { + var nilAny any + var nilStringPtr *string + nonEmpty := "value" + + assert.True(t, IsNilInterface(nil)) + assert.True(t, IsNilInterface(nilAny)) + assert.True(t, IsNilInterface(nilStringPtr)) + assert.False(t, IsNilInterface(nonEmpty)) + assert.False(t, IsNilInterface(&nonEmpty)) +}