From 9d681b6f17b06403b0bba14836d80da0d2ca13a6 Mon Sep 17 00:00:00 2001 From: KANAjetzt <41547570+KANAjetzt@users.noreply.github.com> Date: Wed, 29 Oct 2025 16:33:13 +0100 Subject: [PATCH] feat: :sparkles: enum validation --- scripts/json_schema_validator.gd | 122 ++++++++++++++++-------- test/Unit/test_json_schema_validator.gd | 115 ++++++++++++++++++++++ 2 files changed, 197 insertions(+), 40 deletions(-) diff --git a/scripts/json_schema_validator.gd b/scripts/json_schema_validator.gd index 5df6f00..53d538d 100644 --- a/scripts/json_schema_validator.gd +++ b/scripts/json_schema_validator.gd @@ -86,6 +86,7 @@ const ERR_RANGE_F = "Key %s that equal %f must be %s than %f" const ERR_RANGE_S = "Length of '%s' (%d) %s than declared (%d)" const ERR_WRONG_PATTERN = "String '%s' does not match its corresponding pattern" const ERR_FORMAT = "String '%s' does not match its corresponding format '%s'" +const ERR_ENUM = "Value %s is not one of the allowed enum values for '%s'" # This is one and only function that need you to call outside # If all validation checks passes, this return empty String @@ -109,7 +110,11 @@ func validate(json_data : String, schema: String) -> String: TYPE_DICTIONARY: if parsed_schema.empty(): return "" - elif parsed_schema.keys().size() > 0 && !parsed_schema.has(JSKW_TYPE): + elif ( + parsed_schema.keys().size() > 0 and + not parsed_schema.has(JSKW_TYPE) and + not parsed_schema.has(JSKW_ENUM) + ): return ERR_WRONG_SCHEMA_TYPE _: return ERR_WRONG_SCHEMA_TYPE @@ -136,46 +141,53 @@ func _type_selection(json_data: String, schema: Dictionary, key: String = DEF_KE else: return ERR_INVALID_JSON_GEN + "false is always invalid" - var typearr: Array = _var_to_array(schema.type) var parsed_data = parse_json(json_data) - var error: String = ERR_TYPE_MISMATCH_GEN % [typearr, key] - for type in typearr: - match type: - JST_ARRAY: - if typeof(parsed_data) == TYPE_ARRAY: - error = _validate_array(parsed_data, schema, key) - else: - error = ERR_TYPE_MISMATCH_GEN % [[JST_ARRAY], key] - JST_BOOLEAN: - if typeof(parsed_data) != TYPE_BOOL: - return ERR_TYPE_MISMATCH_GEN % [[JST_BOOLEAN], key] - else: - error = "" - JST_INTEGER: - if typeof(parsed_data) == TYPE_INT: - error = _validate_integer(parsed_data, schema, key) - if typeof(parsed_data) == TYPE_REAL && parsed_data == int(parsed_data): - error = _validate_integer(int(parsed_data), schema, key) - JST_NULL: - if typeof(parsed_data) != TYPE_NIL: - return ERR_TYPE_MISMATCH_GEN % [[JST_NULL], key] - else: - error = "" - JST_NUMBER: - if typeof(parsed_data) == TYPE_REAL: - error = _validate_number(parsed_data, schema, key) - else: - error = ERR_TYPE_MISMATCH_GEN % [[JST_NUMBER], key] - JST_OBJECT: - if typeof(parsed_data) == TYPE_DICTIONARY: - error = _validate_object(parsed_data, schema, key) - else: - error = ERR_TYPE_MISMATCH_GEN % [[JST_OBJECT], key] - JST_STRING: - if typeof(parsed_data) == TYPE_STRING: - error = _validate_string(parsed_data, schema, key) - else: - error = ERR_TYPE_MISMATCH_GEN % [[JST_STRING], key] + var error: String + + if schema.has(JSKW_TYPE): + var typearr: Array = _var_to_array(schema.type) + error = ERR_TYPE_MISMATCH_GEN % [typearr, key] + for type in typearr: + match type: + JST_ARRAY: + if typeof(parsed_data) == TYPE_ARRAY: + error = _validate_array(parsed_data, schema, key) + else: + error = ERR_TYPE_MISMATCH_GEN % [[JST_ARRAY], key] + JST_BOOLEAN: + if typeof(parsed_data) != TYPE_BOOL: + return ERR_TYPE_MISMATCH_GEN % [[JST_BOOLEAN], key] + else: + error = "" + JST_INTEGER: + if typeof(parsed_data) == TYPE_INT: + error = _validate_integer(parsed_data, schema, key) + if typeof(parsed_data) == TYPE_REAL && parsed_data == int(parsed_data): + error = _validate_integer(int(parsed_data), schema, key) + JST_NULL: + if typeof(parsed_data) != TYPE_NIL: + return ERR_TYPE_MISMATCH_GEN % [[JST_NULL], key] + else: + error = "" + JST_NUMBER: + if typeof(parsed_data) == TYPE_REAL: + error = _validate_number(parsed_data, schema, key) + else: + error = ERR_TYPE_MISMATCH_GEN % [[JST_NUMBER], key] + JST_OBJECT: + if typeof(parsed_data) == TYPE_DICTIONARY: + error = _validate_object(parsed_data, schema, key) + else: + error = ERR_TYPE_MISMATCH_GEN % [[JST_OBJECT], key] + JST_STRING: + if typeof(parsed_data) == TYPE_STRING: + error = _validate_string(parsed_data, schema, key) + else: + error = ERR_TYPE_MISMATCH_GEN % [[JST_STRING], key] + + if schema.has(JSKW_ENUM): + error = _validate_enum(parsed_data, schema, key) + return error @@ -511,3 +523,33 @@ func _validate_string(input_data: String, input_schema: Dictionary, property_nam return ERR_INVALID_JSON_GEN % ERR_FORMAT % [property_name, JSKW_COLOR] return error + + +func _enum_match(value, enum_item) -> bool: + var value_type = typeof(value) + var enum_type = typeof(enum_item) + + # Dicts are compared by reference by default - so we use the dicts hash value here. + # (Dictionaries are ordered since Godot 3.0) + # (If hash collisions are an actual issue we have to write a comparison util). + if value_type == TYPE_DICTIONARY and enum_type == TYPE_DICTIONARY: + return value.hash() == enum_item.hash() + + if not value_type == enum_type: + return false + + return value == enum_item + + +func _validate_enum(value, schema: Dictionary, property_name: String = DEF_KEY_NAME) -> String: + # enum must be an array + if typeof(schema[JSKW_ENUM]) != TYPE_ARRAY: + return ERR_WRONG_SCHEMA_GEN + ERR_TYPE_MISMATCH_GEN % [JST_ARRAY, property_name+"."+JSKW_ENUM] + + for item in schema[JSKW_ENUM]: + if _enum_match(value, item): + return "" + + # Not found in enum + var val_str = JSON.print(value) + return ERR_INVALID_JSON_GEN % (ERR_ENUM % [val_str, property_name]) diff --git a/test/Unit/test_json_schema_validator.gd b/test/Unit/test_json_schema_validator.gd index 792de48..8675051 100644 --- a/test/Unit/test_json_schema_validator.gd +++ b/test/Unit/test_json_schema_validator.gd @@ -214,3 +214,118 @@ func test_validate(params = use_parameters(test_is_json_valid_params)) -> void: result == expected_result, "Expected %s but was %s instead for json_data \"%s\" with json_schema \"%s\" and error: %s " % [expected_result, result, json_data, json_schema, error] ) + + +var test_enum_params = [ + ["green", {"enum": ["red", "blue", "green"]}, true], + # number enum match + [2, {"type":"number","enum":[1,2,3]}, true], + # number enum no match + [4, {"type":"number","enum":[1,2,3]}, false], + # string enum match + ["red", {"type":"string","enum":["red","green"]}, true], + # string enum no match + ["blue", {"type":"string","enum":["red","green"]}, false], + # object enum match (deep equality) + [{"b":2}, {"type":"object","enum":[{"a":1},{"b":2}]}, true], + # object enum no match + [{"a":1}, {"type":"object","enum":[{"a":2}]}, false], + # array enum match + [[1,2], {"type":"array","enum":[[1,2],[3,4]]}, true], + # invalid enum schema (not an array) should produce schema error -> treated as failure + [1, {"enum":"notarray"}, false], +] + + +func test_enum_validate(params = use_parameters(test_enum_params)) -> void: + # prepare + var json_data = JSON.print(params[0]) + var json_schema = JSON.print(params[1]) + var expected_result = params[2] + + # test + var json_schema_validator = JSONSchema.new() + var error = json_schema_validator.validate(json_data, json_schema) + + var result = error == "" + + # validate + assert_true( + result == expected_result, + "Expected %s but was %s instead for json_data \"%s\" with json_schema \"%s\" and error: %s " % [expected_result, result, json_data, json_schema, error] + ) + + +var test_enum_edge_params = [ + ["2", {"type":"string", "enum":["small", "medium", "large"]}, false], + # empty enum array -> nothing matches + [1, {"enum":[]}, false], + # null in enum should match null + [null, {"enum":[null, "a"]}, true], + # boolean enum + [true, {"enum":[false, true]}, true], + # mixed-type enum (string vs number) - string "1" should match + ["1", {"enum":[1, "1"]}, true], + # array no match + [[1,2,3], {"type":"array","enum":[[1,2]]}, false], +] + + +func test_enum_edge_validate(params = use_parameters(test_enum_edge_params)) -> void: + # prepare + var json_data = JSON.print(params[0]) + var json_schema = JSON.print(params[1]) + var expected_result = params[2] + + # test + var json_schema_validator = JSONSchema.new() + var error = json_schema_validator.validate(json_data, json_schema) + + var result = error == "" + + # validate + assert_true( + result == expected_result, + "Expected %s but was %s instead for json_data \"%s\" with json_schema \"%s\" and error: %s " % [expected_result, result, json_data, json_schema, error] + ) + + +var test_object_edge_params = [ + # additionalProperties false with extra property -> fail + [{"a":"x","b":"y"}, {"type":"object","properties":{"a":{"type":"string"}},"additionalProperties":false}, false], + # additionalProperties as schema -> extra prop validated by schema + [{"a":"x","b":2}, {"type":"object","properties":{"a":{"type":"string"}},"additionalProperties":{"type":"number"}}, true], + # required property missing -> fail + [{}, {"type":"object","properties":{"a":{"type":"string"}},"required":["a"]}, false], + # propertyNames pattern matching key names + [{"foo":"bar"}, {"type":"object","propertyNames":{"pattern":"^foo$"}}, true], + [{"bar":"baz"}, {"type":"object","propertyNames":{"pattern":"^foo$"}}, false], + # string minLength + ["abc", {"type":"string","minLength":2}, true], + ["a", {"type":"string","minLength":2}, false], + # multipleOf with float + [0.5, {"type":"number","multipleOf":0.25}, true], + [0.3, {"type":"number","multipleOf":0.25}, false], + # exclusiveMinimum + [5, {"type":"number","exclusiveMinimum":5}, false], + [6, {"type":"number","exclusiveMinimum":5}, true], +] + + +func test_object_edge_validate(params = use_parameters(test_object_edge_params)) -> void: + # prepare + var json_data = JSON.print(params[0]) + var json_schema = JSON.print(params[1]) + var expected_result = params[2] + + # test + var json_schema_validator = JSONSchema.new() + var error = json_schema_validator.validate(json_data, json_schema) + + var result = error == "" + + # validate + assert_true( + result == expected_result, + "Expected %s but was %s instead for json_data \"%s\" with json_schema \"%s\" and error: %s " % [expected_result, result, json_data, json_schema, error] + )