Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
f0b05ab
implement abstractted IsBackwardCompatible function
punit-kulal Sep 21, 2023
aa6aad0
update jsonschema dependency
punit-kulal Sep 21, 2023
563a09b
implement explore schema function
punit-kulal Sep 21, 2023
1d213cf
error wrapper and helper functions
punit-kulal Sep 21, 2023
f88657f
bump go version to 1.20 to add support for generics
punit-kulal Sep 22, 2023
52cf006
refactored utils.go function name and few functions
punit-kulal Sep 22, 2023
322bdd9
added static schema uri for equal comparison across schemas for location
punit-kulal Sep 22, 2023
49c1f6b
feat: implemented compatibility feature checks
punit-kulal Sep 22, 2023
056f6a1
filter remote references from backward compatibility check
punit-kulal Sep 22, 2023
4a7f083
feat: added check to restrict additional properties to to enforce ope…
punit-kulal Sep 25, 2023
36937b4
refactor compare schemas for better testability
punit-kulal Sep 25, 2023
c637ae7
add compatibility test for additionalProperties check
punit-kulal Sep 25, 2023
9c3b3f3
add test for compareSchema
punit-kulal Sep 25, 2023
2056f2b
add property deleted test
punit-kulal Sep 25, 2023
d9aa62c
refactor: make type checks injectible for better testing
punit-kulal Sep 25, 2023
a6092bb
added test for type check correctness
punit-kulal Sep 25, 2023
ff37d96
refactor: move helper check functions to compatibility_helper file
punit-kulal Sep 25, 2023
8515119
added test for reference check
punit-kulal Sep 25, 2023
05efc94
added tests for anyOf, allOf and oneOf conditionals
punit-kulal Sep 26, 2023
6fc5f26
added test to check field addition and added formatting
punit-kulal Sep 26, 2023
7eb4812
adde checks for property addition
punit-kulal Sep 26, 2023
1c5c2a2
added stub for tests
punit-kulal Sep 27, 2023
52d1847
feat: add schema for testing item schema
punit-kulal Sep 27, 2023
aca0d4e
implement item schema compatibility checks
punit-kulal Sep 27, 2023
63c8fa8
implement item schema exploration
punit-kulal Sep 27, 2023
4741766
add nil check on type check executor
punit-kulal Oct 3, 2023
ae99ddb
fix: formatting and linting errors
punit-kulal Oct 3, 2023
74b91ff
bump golang version for linting and test-server
punit-kulal Oct 3, 2023
d965d17
bump go version in release pipeline
punit-kulal Oct 3, 2023
3eb51c5
added new line to all json files
punit-kulal Oct 3, 2023
268c93e
fix: comments on compatibility files
punit-kulal Oct 3, 2023
c48dfab
add parameter name for better readability
punit-kulal Oct 3, 2023
696549d
added new line to json file
punit-kulal Oct 3, 2023
ad26c0d
improve correctness of backward compatibility by for oneOf and any Of…
punit-kulal Oct 11, 2023
458a1f2
added formatting
punit-kulal Oct 11, 2023
9d966b6
remove unused oneOf modified
punit-kulal Oct 11, 2023
ab9aac6
removed duplicate enum check
punit-kulal Oct 12, 2023
02a65a3
fixed test when ref is absent
punit-kulal Oct 12, 2023
ea8723e
fix: delete duplicated fields
punit-kulal Oct 12, 2023
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
10 changes: 5 additions & 5 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ jobs:
name: "Lint Go"
runs-on: "ubuntu-latest"
steps:
- uses: actions/setup-go@v2
- uses: actions/setup-go@v4
with:
go-version: "1.17"
- uses: actions/checkout@v2
go-version: "1.20"
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
uses: golangci/golangci-lint-action@v3
with:
skip-go-installation: true
version: v1.54

codeql:
name: "Analyze with CodeQL"
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release-server.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v4
with:
go-version: "1.16"
go-version: "1.20"
- name: Login to DockerHub
uses: docker/login-action@v1
with:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test-server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ jobs:
- 5432:5432
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
uses: actions/setup-go@v4
with:
go-version: ^1.16
go-version: ^1.20
id: go
- name: Install Protoc
uses: arduino/setup-protoc@v1
Expand Down
163 changes: 163 additions & 0 deletions formats/json/compatibility.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package json

import (
"fmt"

"github.com/raystack/stencil/pkg/logger"
"github.com/santhosh-tekuri/jsonschema/v5"
)

const (
_ diffKind = iota
schemaDeleted
incompatibleTypes
requiredFieldChanged
propertyAddition
itemSchemaModification
itemSchemaAddition
itemsSchemaDeletion
subSchemaTypeModification
enumCreation
enumDeletion
enumElementDeletion
refChanged
anyOfModified
anyOfAdded
anyOfDeleted
anyOfElementAdded
anyOfElementDeleted
oneOfAdded
oneOfDeleted
oneOfElementAdded
oneOfElementDeleted
allOfModified
additionalPropertiesNotTrue
)

var backwardCompatibility = []diffKind{
schemaDeleted,
requiredFieldChanged,
itemSchemaAddition,
itemsSchemaDeletion,
incompatibleTypes,
itemSchemaModification,
subSchemaTypeModification,
enumCreation,
enumDeletion,
enumElementDeletion,
refChanged,
anyOfDeleted,
anyOfElementDeleted,
oneOfDeleted,
oneOfElementDeleted,
allOfModified,
additionalPropertiesNotTrue,
}

type SchemaCompareCheck func(prev, curr *jsonschema.Schema, err *compatibilityErr)
type SchemaCheck func(curr *jsonschema.Schema, err *compatibilityErr)

type TypeCheckSpec struct {
emptyTypeChecks []SchemaCompareCheck
objectTypeChecks []SchemaCompareCheck
arrayTypeChecks []SchemaCompareCheck
}

var (
emptyTypeChecks []SchemaCompareCheck = []SchemaCompareCheck{
checkAllOf, checkAnyOf, checkOneOf, checkEnum, checkRef,
}
objectTypeChecks []SchemaCompareCheck = []SchemaCompareCheck{
checkRequiredProperties, checkPropertyAddition,
}
/*
Array schemas can define subschemas for each index as well as for rest of the elements.
Hence, divided the two evaluation into two separate functions.
*/
arrayTypeChecks []SchemaCompareCheck = []SchemaCompareCheck{
checkItemSchema, checkRestOfItemsSchema,
}
)

var StandardTypeChecks TypeCheckSpec = TypeCheckSpec{emptyTypeChecks, objectTypeChecks, arrayTypeChecks}

func compareSchemas(prevSchemaMap, currentSchemaMap map[string]*jsonschema.Schema, notAllowedChanges []diffKind,
schemaCompareFuncs []SchemaCompareCheck, schemaChecks []SchemaCheck) error {
diffs := &compatibilityErr{notAllowed: notAllowedChanges}
for location, prevSchema := range prevSchemaMap {
currSchema := currentSchemaMap[location]
executeSchemaCompareCheck(prevSchema, currSchema, diffs, schemaCompareFuncs)
}
for _, currSchema := range currentSchemaMap {
for _, schemaCheck := range schemaChecks {
schemaCheck(currSchema, diffs)
}
}
if diffs.isEmpty() {
return nil
}
return diffs
}

func CheckPropertyDeleted(prevSchema, currSchema *jsonschema.Schema, diffs *compatibilityErr) {
if prevSchema != nil && currSchema == nil {
diffs.add(schemaDeleted, prevSchema.Location, `property is removed`)
}
}

func CheckAdditionalProperties(schema *jsonschema.Schema, diffs *compatibilityErr) {
/*
enforcing open content model, in the future we can use existing additional properties schema to validate
new properties to ensure better adherence to schema.
*/
if schema.AdditionalProperties != nil {
property, ok := schema.AdditionalProperties.(bool)
if !ok || !property {
diffs.add(additionalPropertiesNotTrue, schema.Location, "additionalProperties need to be not defined or true for evaluation as an open content model")
}
}
}

func TypeCheckExecutor(spec TypeCheckSpec) SchemaCompareCheck {
return func(prevSchema, currSchema *jsonschema.Schema, diffs *compatibilityErr) {
if prevSchema == nil || currSchema == nil {
return
}
prevTypes := prevSchema.Types
currTypes := currSchema.Types
err := elementsMatch(prevTypes, currTypes) // special case of integer being allowed to changed to number is not respected due to additional code complexity
if err != nil {
diffs.add(subSchemaTypeModification, currSchema.Location, err.Error())
return
}
if len(currTypes) == 0 {
/*
types are not available for references and conditional schema types
ref/holder schema
*/
executeSchemaCompareCheck(prevSchema, currSchema, diffs, spec.emptyTypeChecks)
return
}
for _, schemaTypes := range prevTypes {
switch schemaTypes {
case "object":
executeSchemaCompareCheck(prevSchema, currSchema, diffs, spec.objectTypeChecks)
case "array":
// check item schema is same
executeSchemaCompareCheck(prevSchema, currSchema, diffs, spec.arrayTypeChecks)
case "integer":
// check for validation conflicts
case "string":
// check validation conflicts
case "number":
// check validation conflicts
case "boolean":

case "null":

default:
logger.Logger.Warn(fmt.Sprintf("Unexpected type %s", schemaTypes))
}
}
}
}
179 changes: 179 additions & 0 deletions formats/json/compatibility_helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package json

import (
"strings"

"github.com/santhosh-tekuri/jsonschema/v5"
)

func checkEnum(prevSchema, currSchema *jsonschema.Schema, diffs *compatibilityErr) {
prevEnum := prevSchema.Enum
currEnum := currSchema.Enum
if prevEnum == nil && currEnum != nil {
diffs.add(enumCreation, currSchema.Location, "enum values added to existing non enum values")
}
if prevEnum != nil && currEnum == nil {
diffs.add(enumDeletion, currSchema.Location, "enum was deleted")
}
if prevEnum != nil && currEnum != nil {
if !isSubset(currEnum, prevEnum) {
diffs.add(enumElementDeletion, currSchema.Location, "enum property was deleted")
}
}
}

func checkRef(prevSchema, currSchema *jsonschema.Schema, diffs *compatibilityErr) {
if prevSchema.Ref != nil && currSchema.Ref != nil && prevSchema.Ref.Location != currSchema.Ref.Location { // check if prev and curr schema location are equivalent
diffs.add(refChanged, currSchema.Location, "ref for schema has been changed")
}
if prevSchema.Ref != nil && currSchema.Ref == nil {
diffs.add(refChanged, currSchema.Location, "ref for schema has been removed")
}
if prevSchema.Ref == nil && currSchema.Ref != nil {
diffs.add(refChanged, currSchema.Location, "ref for schema has been added")
}
}

func checkAnyOf(prevSchema, currSchema *jsonschema.Schema, diffs *compatibilityErr) {
prevAnyOf := prevSchema.AnyOf
currAnyOf := currSchema.AnyOf
if prevAnyOf != nil && currAnyOf != nil {
if len(prevAnyOf) < len(currAnyOf) {
diffs.add(anyOfElementAdded, currSchema.Location, "anyOf condition cannot have added elements")
return
}
if len(prevAnyOf) > len(currAnyOf) {
diffs.add(anyOfElementDeleted, currSchema.Location, "anyOf condition cannot have deleted elements")
return
}
}
if prevAnyOf == nil && currAnyOf != nil {
diffs.add(anyOfAdded, currSchema.Location, "anyOf condition cannot created during modification of schema")
}
if prevAnyOf != nil && currAnyOf == nil {
diffs.add(anyOfDeleted, currSchema.Location, "anyOf condition cannot be removed during modification of schema")
}
}

func checkOneOf(prevSchema, currSchema *jsonschema.Schema, diffs *compatibilityErr) {
prevOneOf := prevSchema.OneOf
currOneOf := currSchema.OneOf
if prevOneOf != nil && currOneOf != nil {
if len(prevOneOf) < len(currOneOf) {
diffs.add(oneOfElementAdded, currSchema.Location, "oneOf condition cannot have added elements")
return
}
if len(prevOneOf) > len(currOneOf) {
diffs.add(oneOfElementDeleted, currSchema.Location, "oneOf condition cannot have elements removed")
}
}
if prevOneOf == nil && currOneOf != nil {
diffs.add(oneOfAdded, currSchema.Location, "oneOf condition cannot created during modification of schema")
}
if prevOneOf != nil && currOneOf == nil {
diffs.add(oneOfDeleted, currSchema.Location, "oneOf condition cannot be removed during modification of schema")
}
}

func checkAllOf(prevSchema, currSchema *jsonschema.Schema, diffs *compatibilityErr) {
prevAllOf := prevSchema.AllOf
currAllOf := currSchema.AllOf
if prevAllOf != nil && currAllOf != nil {
if len(prevAllOf) != len(currAllOf) {
diffs.add(allOfModified, currSchema.Location, "allOf condition cannot be modified")
return
}
}
if prevAllOf == nil && currAllOf != nil {
diffs.add(allOfModified, currSchema.Location, "allOf condition cannot created during modification of schema")
}
if prevAllOf != nil && currAllOf == nil {
diffs.add(allOfModified, currSchema.Location, "allOf condition cannot be removed during modification of schema")
}
}

func checkItemSchema(prevSchema, currSchema *jsonschema.Schema, diffs *compatibilityErr) {
// check index based schemas
if prevSchema.Draft == jsonschema.Draft2020 {
prevItems := prevSchema.PrefixItems
currItems := currSchema.PrefixItems
if prevItems != nil && currItems != nil {
if len(prevItems) != len(currItems) {
diffs.add(itemSchemaModification, currSchema.Location, "prev prefix items contains %d elements, current contains %d", len(prevItems), len(currItems))
}
}
if prevItems == nil && currItems != nil {
diffs.add(itemSchemaModification, currSchema.Location, "prev prefix items is absent, current contains %d", len(currItems))
}
if prevItems != nil && currItems == nil {
diffs.add(itemSchemaModification, currSchema.Location, "prev prefix items contains %d elements, current contains absent", len(prevItems))
}
} else {
prevItems := getItems(prevSchema)
currItems := getItems(currSchema)
if len(prevItems) != len(currItems) {
diffs.add(itemSchemaModification, currSchema.Location, "prev items contains %d elements, current contains %d", len(prevItems), len(currItems))
}
}
}

func checkRestOfItemsSchema(prevSchema, currSchema *jsonschema.Schema, diffs *compatibilityErr) {
var prevItem, currItem *jsonschema.Schema
var ok bool
// check schema for remaining array elements
if prevSchema.Draft == jsonschema.Draft2020 {
prevItem = prevSchema.Items2020
currItem = currSchema.Items2020
} else {
if prevSchema.AdditionalItems != nil {
prevItem, ok = prevSchema.AdditionalItems.(*jsonschema.Schema)
if !ok { // prev schema additional Items is boolean value
if prevSchema.AdditionalItems != currSchema.AdditionalItems {
// curr schema additional items is not equivalent
diffs.add(itemSchemaModification, prevSchema.Location, "the value of additional items has changed")
}
return // since both cases equal and non equal have been evaluated.
}
}
if currSchema.AdditionalItems != nil {
currItem, ok = currSchema.AdditionalItems.(*jsonschema.Schema)
if !ok { // curr schema is boolean
if prevSchema.AdditionalItems == nil {
diffs.add(itemSchemaAddition, prevSchema.Location, "additional items has been set, changes are not allowed to additional items")
} else if prevSchema.AdditionalItems != currSchema.AdditionalItems {
diffs.add(itemSchemaModification, prevSchema.Location, "additional items has been modified, changes are not allowed")
}
return
}
}
}
if prevItem == nil && currItem != nil {
diffs.add(itemSchemaAddition, currItem.Location, "item schema cannot be added in schema changes")
} else if prevItem != nil && currItem == nil {
diffs.add(itemsSchemaDeletion, prevItem.Location, "items schema cannot be deleted in modification changes")
}
}

func checkPropertyAddition(prevSchema, currSchema *jsonschema.Schema, diffs *compatibilityErr) {
prevProperties := getKeys(prevSchema.Properties)
currProperties := getKeys(currSchema.Properties)
addedKeys := getDiffernce(currProperties, prevProperties)
if len(addedKeys) > 0 {
diffs.add(propertyAddition, currSchema.Location, "added keys: %s", strings.Join(addedKeys, ","))
}
}

func checkRequiredProperties(prevSchema, currSchema *jsonschema.Schema, diffs *compatibilityErr) {
prevRequiredProperties := prevSchema.Required
currReqiredProperties := currSchema.Required
err := elementsMatch(prevRequiredProperties, currReqiredProperties)
if err != nil {
diffs.add(requiredFieldChanged, currSchema.Location, err.Error())
}
}

func executeSchemaCompareCheck(prev, curr *jsonschema.Schema, diffs *compatibilityErr, checks []SchemaCompareCheck) {
for _, check := range checks {
check(prev, curr, diffs)
}
}
Loading