Skip to content
Open
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
142 changes: 117 additions & 25 deletions scripts/json_schema_validator.gd
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,19 @@ const JSM_LESS = "less"
const JSM_LESS_EQ = "less or equal"
const JSM_OBJ_DICT = "object (dictionary)"

const JSL_AND = "%s and %s"
const JSL_OR = "%s or %s"

const ERR_SCHEMA_FALSE = "Schema declared as deny all"
const ERR_WRONG_SCHEMA_GEN = "Schema error: "
const ERR_WRONG_SCHEMA_TYPE = "Schema error: schema must be empty object or object with 'type' keyword or boolean value"
const ERR_WRONG_SHEMA_NOTA = "Schema error: expected that all elements of '%s.%s' must be '%s'"
const ERR_WRONG_PROP_TYPE = "Schema error: any schema item must be object with 'type' keyword"
const ERR_REQ_PROP_GEN = "Schema error: expected array of required properties for '%s'"
const ERR_REQ_PROP_MISSING = "Missing required property: '%s' for '%s'"
const ERR_NO_PROP_ADD = "Additional properties are not required: found '%s'"
const ERR_FEW_PROP = "%d propertie(s) are not enough properties, at least %d are required"
const ERR_MORE_PROP = "%d propertie(s) are too many properties, at most %d are allowed"
const ERR_INVALID_JSON_GEN = "Validation fails with message: %s"
const ERR_INVALID_JSON_EXT = "Invalid JSON data passed with message: %s"
const ERR_TYPE_MISMATCH_GEN = "Type mismatch: expected %s for '%s'"
Expand All @@ -67,6 +73,7 @@ const ERR_MULT_F = "Key %s that equal %f must be multiple of %f"
const ERR_RANGE_D = "Key %s that equal %d must be %s than %d"
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"

# This is one and only function that need you to call outside
# If all validation checks passes, this return empty String
Expand All @@ -88,7 +95,9 @@ func validate(json_data : String, schema: String) -> String:
else:
return ""
TYPE_DICTIONARY:
if parsed_schema.keys().size() > 0 && !parsed_schema.has(JSKW_TYPE):
if parsed_schema.empty():
return ""
elif parsed_schema.keys().size() > 0 && !parsed_schema.has(JSKW_TYPE):
return ERR_WRONG_SCHEMA_TYPE
_: return ERR_WRONG_SCHEMA_TYPE

Expand Down Expand Up @@ -195,16 +204,10 @@ func _validate_number(input_data: float, input_schema: Dictionary, property_name
# integer mode turns on only if types has integer and has not number
var integer_mode: bool = types.has(JST_INTEGER) && !types.has(JST_NUMBER)

# defining minimums and maximums (if exclusions not present it's consider as false)
var min_ex : bool = input_schema.has(JSKW_MIN_EX) && input_schema[JSKW_MIN_EX]
var min_ex_msg: String = JSM_GREATER if min_ex else JSM_GREATER_EQ
var max_ex : bool = input_schema.has(JSKW_MAX_EX) && input_schema[JSKW_MAX_EX]
var max_ex_msg: String = JSM_LESS if max_ex else JSM_LESS_EQ

# processing multiple check
if input_schema.has(JSKW_MULT_OF):
var mult = float(input_schema[JSKW_MULT_OF]) if input_schema[JSKW_MULT_OF] else 0.0
mult = int(input_schema[JSKW_MULT_OF]) if integer_mode else mult
var mult = float(input_schema[JSKW_MULT_OF])
mult = int(mult) if integer_mode else mult
if fmod(input_data, mult) != 0:
if integer_mode:
return ERR_MULT_D % [property_name, input_data, mult]
Expand All @@ -213,32 +216,78 @@ func _validate_number(input_data: float, input_schema: Dictionary, property_name

# processing minimum check
if input_schema.has(JSKW_MINIMUM):
var minimum = float(input_schema[JSKW_MINIMUM]) if input_schema[JSKW_MINIMUM] else 0.0
minimum = int(input_schema[JSKW_MINIMUM]) if integer_mode else minimum
var suberror : bool = (input_data <= minimum) if min_ex else (input_data < minimum)
if suberror:
var minimum = float(input_schema[JSKW_MINIMUM])
minimum = int(minimum) if integer_mode else minimum
if input_data < minimum:
if integer_mode:
return ERR_RANGE_D % [property_name, input_data, JSM_GREATER_EQ, minimum]
else:
return ERR_RANGE_F % [property_name, input_data, JSM_GREATER_EQ, minimum]

# processing exclusive minimum check
if input_schema.has(JSKW_MIN_EX):
var minimum = float(input_schema[JSKW_MIN_EX])
minimum = int(minimum) if integer_mode else minimum
if input_data <= minimum:
if integer_mode:
return ERR_RANGE_D % [property_name, input_data, min_ex_msg, minimum]
return ERR_RANGE_D % [property_name, input_data, JSM_GREATER, minimum]
else:
return ERR_RANGE_F % [property_name, input_data, min_ex_msg, minimum]
return ERR_RANGE_F % [property_name, input_data, JSM_GREATER, minimum]

# processing maximum check
if input_schema.has(JSKW_MAXIMUM):
var maximum = float(input_schema[JSKW_MAXIMUM]) if input_schema[JSKW_MAXIMUM] else 0.0
maximum = int(input_schema[JSKW_MAXIMUM]) if integer_mode else maximum
var suberror : bool = (input_data >= maximum) if max_ex else (input_data > maximum)
if suberror:
var maximum = float(input_schema[JSKW_MAXIMUM])
maximum = int(maximum) if integer_mode else maximum
if input_data > maximum:
if integer_mode:
return ERR_RANGE_D % [property_name, input_data, max_ex_msg, maximum]
return ERR_RANGE_D % [property_name, input_data, JSM_LESS_EQ, maximum]
else:
return ERR_RANGE_F % [property_name, input_data, max_ex_msg, maximum]
return ERR_RANGE_F % [property_name, input_data, JSM_LESS_EQ, maximum]

# processing exclusive minimum check
if input_schema.has(JSKW_MAX_EX):
var maximum = float(input_schema[JSKW_MAX_EX])
maximum = int(maximum) if integer_mode else maximum
if input_data >= maximum:
if integer_mode:
return ERR_RANGE_D % [property_name, input_data, JSM_LESS, maximum]
else:
return ERR_RANGE_F % [property_name, input_data, JSM_LESS, maximum]

return ""

func _validate_object(input_data: Dictionary, input_schema: Dictionary, property_name: String = DEF_KEY_NAME) -> String:
# TODO: additionalProperties patternProperties propertyNames minProperties maxProperties dependencies
# TODO: patternProperties
var error : String = ""


# Process dependencies
if input_schema.has(JSKW_DEPEND):
for dependency in input_schema.dependencies.keys():
if input_data.has(dependency):
match typeof(input_schema.dependencies[dependency]):
TYPE_ARRAY:
if input_schema.has(JSKW_REQ):
for property in input_schema.dependencies[dependency]:
input_schema.required.append(property)
else:
input_schema.required = input_schema.dependencies[dependency]
TYPE_DICTIONARY:
for key in input_schema.dependencies[dependency].keys():
if input_schema.has(key):
match typeof(input_schema[key]):
TYPE_ARRAY:
for element in input_schema.dependencies[dependency][key]:
input_schema[key].append(element)
TYPE_DICTIONARY:
for element in input_schema.dependencies[dependency][key].keys():
input_schema[key][element] = input_schema.dependencies[dependency][key][element]
_:
input_schema[key] = input_schema.dependencies[dependency][key]
else:
input_schema[key] = input_schema.dependencies[dependency][key]
_:
return ERR_WRONG_SCHEMA_GEN + ERR_TYPE_MISMATCH_GEN % [JSL_OR % [JST_ARRAY, JSM_OBJ_DICT], property_name]

# Process properties
if input_schema.has(JSKW_PROP):

Expand All @@ -256,17 +305,52 @@ func _validate_object(input_data: Dictionary, input_schema: Dictionary, property
for key in input_schema.properties:
if !input_schema.properties[key].has(JSKW_TYPE):
return ERR_WRONG_PROP_TYPE
# TODO: additional properties check
if input_data.has(key):
error = _type_selection(JSON.print(input_data[key]), input_schema.properties[key], key)
else:
pass
if error: return error

# Process additional properties
if input_schema.has(JSKW_PROP_ADD):
match typeof(input_schema.additionalProperties):
TYPE_BOOL:
if not input_schema.additionalProperties:
for key in input_data:
if not input_schema.properties.has(key):
return ERR_NO_PROP_ADD % key
TYPE_DICTIONARY:
for key in input_data:
if not input_schema.properties.has(key):
return _type_selection(JSON.print(input_data[key]), input_schema.additionalProperties, key)
_:
return ERR_WRONG_SCHEMA_GEN + ERR_TYPE_MISMATCH_GEN % [JSL_OR % [JST_BOOLEAN, JSM_OBJ_DICT], property_name]

# Process properties names
if input_schema.has(JSKW_PROP_NAMES):
if typeof(input_schema.propertyNames) != TYPE_DICTIONARY:
return ERR_WRONG_SCHEMA_GEN + ERR_TYPE_MISMATCH_GEN % [JSM_OBJ_DICT, property_name]
for key in input_data:
error = _validate_string(key, input_schema.propertyNames, key)
if error: return error

# Process minProperties maxProperties
if input_schema.has(JSKW_PROP_MIN):
if typeof(input_schema[JSKW_PROP_MIN]) != TYPE_REAL:
return ERR_WRONG_SCHEMA_GEN + ERR_TYPE_MISMATCH_GEN % [JST_INTEGER, property_name]
if input_data.keys().size() < input_schema[JSKW_PROP_MIN]:
return ERR_FEW_PROP % [input_data.keys().size(), input_schema[JSKW_PROP_MIN]]

if input_schema.has(JSKW_PROP_MAX):
if typeof(input_schema[JSKW_PROP_MAX]) != TYPE_REAL:
return ERR_WRONG_SCHEMA_GEN + ERR_TYPE_MISMATCH_GEN % [JST_INTEGER, property_name]
if input_data.keys().size() > input_schema[JSKW_PROP_MAX]:
return ERR_MORE_PROP % [input_data.keys().size(), input_schema[JSKW_PROP_MAX]]

return error

func _validate_string(input_data: String, input_schema: Dictionary, property_name: String = DEF_KEY_NAME) -> String:
# TODO: pattern, format
# TODO: format
var error : String = ""
if input_schema.has(JSKW_LENGTH_MIN):
if not (typeof(input_schema[JSKW_LENGTH_MIN]) == TYPE_INT || typeof(input_schema[JSKW_LENGTH_MIN]) == TYPE_REAL):
Expand All @@ -280,4 +364,12 @@ func _validate_string(input_data: String, input_schema: Dictionary, property_nam
if input_data.length() > input_schema[JSKW_LENGTH_MAX]:
return ERR_INVALID_JSON_GEN % ERR_RANGE_S % [property_name, input_data.length(), JSM_GREATER, input_schema[JSKW_LENGTH_MAX]]

if input_schema.has(JSKW_PATTERN):
if not (typeof(input_schema[JSKW_PATTERN]) == TYPE_STRING):
return ERR_TYPE_MISMATCH_GEN % [JST_STRING, property_name+"."+JSKW_PATTERN]
var regex = RegEx.new()
regex.compile(input_schema[JSKW_PATTERN])
if regex.search(input_data) == null:
return ERR_INVALID_JSON_GEN % ERR_WRONG_PATTERN % property_name

return error