Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/20260527132139.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:sparkles: `[yaml]` Added helpers for checking valid files extensions
1 change: 1 addition & 0 deletions changes/20260527132146.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:sparkles: `[json]` Added helpers for checking valid files extensions
1 change: 1 addition & 0 deletions changes/20260527132214.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:gear: `[jsonschema]` Extend JSON Schema validation with schema options and support for schemas defined in YAML.
12 changes: 11 additions & 1 deletion utils/serialization/json/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,16 @@ import (
"github.com/pquerna/ffjson/ffjson"
sigsyaml "sigs.k8s.io/yaml"

"github.com/ARM-software/golang-utils/utils/collection"
"github.com/ARM-software/golang-utils/utils/commonerrors"
"github.com/ARM-software/golang-utils/utils/reflection"
)

var nullBytes = []byte("null")
var (
nullBytes = []byte("null")
// JSONExtensions is the list of file extensions that are considered JSON files.
JSONExtensions = []string{".json"}
)

// Marshal encodes a value to a JSON byte slice.
// It matches encoding/json.Marshal.
Expand Down Expand Up @@ -98,3 +103,8 @@ func ToYAML(rawJSON []byte) (yaml []byte, err error) {
}
return
}

// IsJSON returns true if the extension is a JSON file
func IsJSON(extension string) bool {
return collection.In(JSONExtensions, extension, collection.StringCleanCaseInsensitiveMatch)
}
35 changes: 35 additions & 0 deletions utils/serialization/json/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,41 @@ func TestToYAML(t *testing.T) {
assert.Contains(t, string(output), "count: 2")
}

func TestToYAMLIntegrationInspiredByKubernetesSigsYAML(t *testing.T) {
// Tests inspired by https://github.com/kubernetes-sigs/yaml/blob/master/yaml_test.go
tests := map[string]struct {
json string
expectedContains []string
}{
"string value": {
json: `{"t":"a"}`,
expectedContains: []string{"t: a"},
},
"boolean value": {
json: `{"t":true}`,
expectedContains: []string{"t: true"},
},
"array": {
json: `[{"t":"a"}]`,
expectedContains: []string{"- t: a"},
},
"large integer": {
json: `{"t":9007199254740993}`,
expectedContains: []string{"t: 9007199254740993"},
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
output, err := ToYAML([]byte(test.json))
require.NoError(t, err)
for _, expected := range test.expectedContains {
assert.Contains(t, string(output), expected)
}
})
}
}

func TestToYAMLInvalidJSON(t *testing.T) {
_, err := ToYAML([]byte(`{"name":`))
require.Error(t, err)
Expand Down
11 changes: 11 additions & 0 deletions utils/serialization/yaml/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,16 @@ import (

sigsyaml "sigs.k8s.io/yaml"

"github.com/ARM-software/golang-utils/utils/collection"
"github.com/ARM-software/golang-utils/utils/commonerrors"
jsonserialization "github.com/ARM-software/golang-utils/utils/serialization/json" //nolint:misspell
)

var (
// YAMLExtensions is the list of file extensions that are considered YAML files.
YAMLExtensions = []string{".yaml", ".yml"}
)

// ToJSON converts YAML data to JSON.
//
// YAML parsing support comes from sigs.k8s.io/yaml, which uses yaml/go-yaml
Expand Down Expand Up @@ -91,3 +97,8 @@ func Unmarshal(data []byte, v any) error {
func UnmarshallWithContext(ctx context.Context, data []byte, v any) error {
return NewDecoder(ctx, bytes.NewReader(data)).Decode(v)
}

// IsYAMLFile returns true if the given extension is a YAML file extension.
func IsYAMLFile(extension string) bool {
return collection.In(YAMLExtensions, extension, collection.StringCleanCaseInsensitiveMatch)
}
33 changes: 33 additions & 0 deletions utils/serialization/yaml/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,39 @@ func TestToJSON(t *testing.T) {
assert.JSONEq(t, `{"count":2,"name":"value"}`, string(output))
}

func TestToJSONIntegrationInspiredByKubernetesSigsYAML(t *testing.T) {
// Tests inspired by https://github.com/kubernetes-sigs/yaml/blob/master/yaml_test.go
tests := map[string]struct {
yaml string
json string
}{
"string value": {
yaml: "t: a\n",
json: `{"t":"a"}`,
},
"boolean value": {
yaml: "t: True\n",
json: `{"t":true}`,
},
"array": {
yaml: "- t: a\n",
json: `[{"t":"a"}]`,
},
"large integer": {
yaml: "t: 9007199254740993\n",
json: `{"t":9007199254740993}`,
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
output, err := ToJSON([]byte(test.yaml))
require.NoError(t, err)
assert.JSONEq(t, test.json, string(output))
})
}
}

func TestToJSONInvalidYAML(t *testing.T) {
_, err := ToJSON([]byte("name: [value\n"))
require.Error(t, err)
Expand Down
13 changes: 13 additions & 0 deletions utils/validation/jsonschema/testdata/person.schema.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
$id: testdata/person.schema.yaml
type: object
$defs:
stringField: &stringField
type: string
integerField: &integerField
type: integer
properties:
name: *stringField
count: *integerField
required:
- name
additionalProperties: false
10 changes: 9 additions & 1 deletion utils/validation/jsonschema/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"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
"github.com/ARM-software/golang-utils/utils/serialization/yaml" //nolint:misspell
)

// Schema describes a schema file that can be loaded and registered for
Expand Down Expand Up @@ -120,11 +121,18 @@ func LoadSchemaSpec(ctx context.Context, schema *Schema) (*SchemaSpec, error) {
if err != nil {
return nil, err
}
schemaPath := filesystem.FilePathClean(schema.Filesystem, schema.LocalPath)

data, err := schema.Filesystem.ReadFileWithContextAndLimits(ctx, filesystem.FilePathClean(schema.Filesystem, schema.LocalPath), schema.Limits)
data, err := schema.Filesystem.ReadFileWithContextAndLimits(ctx, schemaPath, schema.Limits)
if err != nil {
return nil, commonerrors.DescribeCircumstancef(err, "failed to load JSON Schema [%v] from [%v]", schema.Title, schema.LocalPath)
}
if yaml.IsYAMLFile(filesystem.FilePathExt(schema.Filesystem, schemaPath)) {
data, err = yaml.ToJSON(data)
if err != nil {
return nil, commonerrors.DescribeCircumstancef(err, "failed to convert schema from YAML to JSON [%v]", schema.Title)
}
}

return &SchemaSpec{
ID: schemaID(schema.ID, schema.LocalPath),
Expand Down
45 changes: 45 additions & 0 deletions utils/validation/jsonschema/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ func newMissingSchema(t *testing.T, fs filesystem.FS) *Schema {
)
}

func newYAMLValidSchema(t *testing.T, fs filesystem.FS) *Schema {
t.Helper()
return NewJSONSchemaFile(
WithTitle("person yaml schema"),
WithLocalPath(path.Join("testdata", "person.schema.yaml")),
WithFilesystem(fs),
)
}

func newEmbeddedFilesystem(t *testing.T) filesystem.FS {
t.Helper()
fs, err := filesystem.NewEmbedFileSystem(&embeddedFS)
Expand Down Expand Up @@ -150,6 +159,13 @@ func TestLoadSchemaSpecEmbeddedFS(t *testing.T) {
assert.NotEmpty(t, spec.Specification)
}

func TestLoadSchemaSpecYAMLSchemaFile(t *testing.T) {
spec, err := LoadSchemaSpec(context.Background(), newYAMLValidSchema(t, filesystem.GetGlobalFileSystem()))
require.NoError(t, err)
assert.NotEmpty(t, spec.Specification)
assert.JSONEq(t, `{"$id":"testdata/person.schema.yaml","type":"object","$defs":{"stringField":{"type":"string"},"integerField":{"type":"integer"}},"properties":{"name":{"type":"string"},"count":{"type":"integer"}},"required":["name"],"additionalProperties":false}`, string(spec.Specification))
}

func TestLoadSchemaSpec_MissingSchemaPath(t *testing.T) {
_, err := LoadSchemaSpec(context.Background(), newMissingSchema(t, filesystem.GetGlobalFileSystem()))
require.Error(t, err)
Expand Down Expand Up @@ -414,6 +430,30 @@ func TestNewJSONFileValidatorWithOptions_LowSchemaFileLimit(t *testing.T) {
errortest.AssertError(t, err, commonerrors.ErrTooLarge)
}

func TestValidateFileWithLimits(t *testing.T) {
v, err := NewJSONFileValidator(nil, *newValidSchema(t, filesystem.GetGlobalFileSystem()))
require.NoError(t, err)

validator, ok := v.(*fileValidator)
require.True(t, ok)

err = validator.ValidateFileWithLimits(context.Background(), path.Join("testdata", "valid.json"), filesystem.NewLimits(0, 1024, 1, 1, false))
require.Error(t, err)
errortest.AssertError(t, err, commonerrors.ErrTooLarge)
}

func TestValidateFileInFSWithLimits(t *testing.T) {
v, err := NewJSONFileValidator(nil, *newValidSchema(t, newEmbeddedFilesystem(t)))
require.NoError(t, err)

validator, ok := v.(*fileValidator)
require.True(t, ok)

err = validator.ValidateFileInFSWithLimits(context.Background(), newEmbeddedFilesystem(t), path.Join("testdata", "valid.json"), filesystem.NewLimits(0, 1024, 1, 1, false))
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)
Expand Down Expand Up @@ -461,6 +501,11 @@ func TestValidateYAMLFileAgainstSchema(t *testing.T) {
require.NoError(t, err)
}

func TestValidateJSONAgainstYAMLSchema(t *testing.T) {
err := ValidateJSONFileAgainstSchema(context.Background(), path.Join("testdata", "valid.json"), nil, *newYAMLValidSchema(t, filesystem.GetGlobalFileSystem()))
require.NoError(t, err)
}

func TestValidateYAMLFileAgainstSchemaOptions(t *testing.T) {
err := ValidateYAMLFileAgainstSchemaOptions(
context.Background(),
Expand Down
4 changes: 2 additions & 2 deletions utils/validation/jsonschema/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func newSchemaCreationFunc(schemaID *string, schema ...Schema) func(context.Cont
func NewJSONFileValidator(schemaID *string, schema ...Schema) (v ISchemaValidator, err error) {
v = &fileValidator{
schemaCreationFunc: newSchemaCreationFunc(schemaID, schema...),
expectedExtensions: []string{".json"},
expectedExtensions: jsonserialization.JSONExtensions,
convertFunc: nil,
}
return
Expand All @@ -125,7 +125,7 @@ func NewJSONFileValidatorWithOptions(options ...SchemaOption) (ISchemaValidator,
func NewYAMLFileValidator(schemaID *string, schema ...Schema) (v ISchemaValidator, err error) {
v = &fileValidator{
schemaCreationFunc: newSchemaCreationFunc(schemaID, schema...),
expectedExtensions: []string{".yaml", ".yml"},
expectedExtensions: yaml.YAMLExtensions,
convertFunc: yaml.ToJSON,
}
return
Expand Down
Loading