Added validation module

Hopefully it works, because I didn't write tests for it. Heck I didn't even test it at all, gonna do that soon.
This commit is contained in:
JeremyStar™ 2024-05-03 21:28:07 +02:00
parent a32b00d118
commit d608999990
Signed by: JeremyStarTM
GPG key ID: E366BAEF67E4704D
4 changed files with 332 additions and 1 deletions

View file

@ -29,3 +29,5 @@ enum LoggerLevel { NONE, SPECIAL, ERROR, WARN, INFO, VERB, DIAG }
enum SceneType { NONE, DEBUG, CUTSCENE, MENU, MAIN, BACKGROUND }
## To what degree [i]something[/i] should be blocked.
enum BlockadeLevel { IGNORE, WARN, BLOCK }
## All rule types data can be checked against.
enum ValidationType { MATCHES_TYPE, MATCHES_CLASS, IN_RANGE, HAS_MINIMUM, HAS_MAXIMUM, HAS_VALUES, CONTAINS, MATCHES_REGEX, IS_NOT_EMPTY, IS_NOT_NULL, IS_NORMALIZED, IS_ORTHONORMALIZED }

View file

@ -0,0 +1,280 @@
extends Node
class_name CoreValidationSingle
## Internal, don't modify.
var core: Core
## Internal, don't modify.
var logger: CoreLoggerInstance
## Is set to the parent owning the validation single.[br]
## [b]Note: [i]Don't modify.[/i][/b]
var parent: Node
## Contains the data to validate.[br]
## [b]Note: [i]Don't modify.[/i][/b]
var data # This is the only instance where we don't want to define what type this variable should have
## All rules to evalute in [method evaluate].[br]
## [b]Note: [i]Don't modify.[/i][/b]
var rules: Array[Dictionary]
## The amount of failures.[br]
## [b]Note: [i]Don't modify.[/i][/b]
var failures: Array[String] = []
# +++ constructor +++
func _init(core_new: Core, data_new) -> void:
core = core_new
logger = core.logger.get_instance(core.basepath.replace("res://", "") + "src/classes/validationsingle.gd", self)
data = data_new
# +++ evaluation +++
## Evaluates all set rules and returns [code]true[/code] if all rules passed, or [code]false[/code] if at least one failed.
func evaluate() -> bool:
# Reset failures
failures = []
# Iterate through all rules
for rule in rules:
match(rule["type"]):
CoreTypes.ValidationType.MATCHES_TYPE:
var success: bool = false
for type in rule["types"]:
if typeof(data) == type:
success = true
if !success:
var types_string: String = ""
for type in rule["types"]:
if types_string == "": types_string = type_string(type)
else: types_string = types_string + "|" + type_string(type)
failures.append(core.stringify_variables("Data is not of expected type(s) %expected% (is %got%)", { "expected": types_string, "got": type_string(typeof(data)) }))
CoreTypes.ValidationType.MATCHES_CLASS:
# If not an object, skip
if typeof(data) != Variant.Type.TYPE_OBJECT:
logger.warn("Can't match class as data is not of type Object")
continue
if rule["exact"]:
# Only check if class name matches specified class
if data.get_class() != rule["class"]:
failures.append(core.stringify_variables("Expected exact class %expected% but data is of class %got%", { "expected": rule["class"], "got": data.get_class() }))
else:
# Check if class is or inherits specified class
if !data.is_class(rule["class"]):
failures.append(core.stringify_variables("Expected class %expected% but data is of class %got%", { "expected": rule["class"], "got": data.get_class() }))
CoreTypes.ValidationType.IN_RANGE:
# If not an int or float, skip
match(rule["matched_against"]):
"integer":
if typeof(data) != Variant.Type.TYPE_INT:
logger.warn("Can't match class as data is not of type int")
continue
"float":
if typeof(data) != Variant.Type.TYPE_FLOAT:
logger.warn("Can't match class as data is not of type float")
continue
_: logger.crash(core.stringify_variables("Invalid 'matched_against' value %value% for IN_RANGE", { "value": rule["matched_against"] }))
# Check if from is bigger than to
if rule["from"] > rule["to"]:
logger.error(core.stringify_variables("Can't match invalid %type% range %from% to %to%", { "type": rule["matched_against"], "from": rule["from"], "to": rule["to"] }))
continue
# Perform checks
if data < rule["from"]: failures.append(core.stringify_variables("Data is smaller than %type% range start %expected%", { "type": rule["matched_against"], "expected": rule["from"] }))
if data > rule["to"]: failures.append(core.stringify_variables("Data is larger than %type% range end %expected%", { "type": rule["matched_against"], "expected": rule["to"] }))
CoreTypes.ValidationType.HAS_MINIMUM:
# If not an int or float, skip
match(rule["matched_against"]):
"integer":
if typeof(data) != Variant.Type.TYPE_INT:
logger.warn("Can't determine if data has minimum value as data is not of type int")
continue
"float":
if typeof(data) != Variant.Type.TYPE_FLOAT:
logger.warn("Can't determine if data has minimum value as data is not of type float")
continue
_: logger.crash(core.stringify_variables("Invalid 'matched_against' value %value% for HAS_MINIMUM", { "value": rule["matched_against"] }))
# Perform check
if data < rule["minimum"]: failures.append(core.stringify_variables("Data is smaller than minimum %type% value %expected%", { "type": rule["matched_against"], "expected": rule["minimum"] }))
CoreTypes.ValidationType.HAS_MAXIMUM:
# If not an int or float, skip
match(rule["matched_against"]):
"integer":
if typeof(data) != Variant.Type.TYPE_INT:
logger.warn("Can't determine if data has maximum value as data is not of type int")
continue
"float":
if typeof(data) != Variant.Type.TYPE_FLOAT:
logger.warn("Can't determine if data has maximum value as data is not of type float")
continue
_: logger.crash(core.stringify_variables("Invalid 'matched_against' value %value% for HAS_MINIMUM", { "value": rule["matched_against"] }))
# Perform check
if data > rule["maximum"]: failures.append(core.stringify_variables("Data is smaller than minimum %type% value %expected%", { "type": rule["matched_against"], "expected": rule["maximum"] }))
CoreTypes.ValidationType.HAS_VALUES:
# Used later
var success: bool = false
if rule["all_required"]:
# Invert usage
success = true
for value in rule["values"]:
if data != value:
# One value did not match
success = false
if !success: failures.append("Data did not match all provided values")
else:
for value in rule["values"]:
if data == value:
# One value did match
success = true
if !success: failures.append("Data did not match a single provided value")
CoreTypes.ValidationType.CONTAINS:
# If not a String or StringName, skip
if typeof(data) != Variant.Type.TYPE_STRING or typeof(data) != Variant.Type.TYPE_STRING_NAME:
logger.warn("Can't determine if data contains values as data is not of type String or StringName")
continue
var successes: int = 0
for value in rule["values"]:
if data.contains(value):
successes += 1
# Success if equals or bigger than 'minimum_matches'
if successes < rule["minimum_matches"]: failures.append(core.stringify_variables("Data did matched %got% out of %expected% expected strings", { "got": successes, "expected": rule["minimum_matches"] }))
CoreTypes.ValidationType.MATCHES_REGEX:
# If not a String or StringName, skip
if typeof(data) != Variant.Type.TYPE_STRING or typeof(data) != Variant.Type.TYPE_STRING_NAME:
logger.warn("Can't determine if data matches regex as data is not of type String or StringName")
continue
var regex: RegEx = RegEx.new()
# Compile regex
regex.compile(rule["regex_string"])
# Get result
var result: RegExMatch = regex.search(data)
# If result yielded no result, fail
if !result: failures.append(core.stringify_variables("Data doesn't match regex %regex%", { "regex": rule["regex_string"] }))
CoreTypes.ValidationType.IS_NOT_EMPTY:
match(typeof(data)):
Variant.Type.TYPE_STRING: if data == "": failures.append("Data string is empty")
Variant.Type.TYPE_STRING_NAME: if data == "": failures.append("Data string name is empty")
Variant.Type.TYPE_INT: if data == 0: failures.append("Data integer is zero")
Variant.Type.TYPE_FLOAT: if data == 0.0: failures.append("Data float is zero")
# If not a String, StringName, int or float, skip
_: logger.warn("Can't determine if data is null as data is not of type String, StringName, int or float")
CoreTypes.ValidationType.IS_NOT_NULL:
# ⡴⠑⡄⠀⠀⠀⠀⠀⠀⠀ ⣀⣀⣤⣤⣤⣀⡀
# ⠸⡇⠀⠿⡀⠀⠀⠀⣀⡴⢿⣿⣿⣿⣿⣿⣿⣿⣷⣦⡀
# ⠀⠀⠀⠀⠑⢄⣠⠾⠁⣀⣄⡈⠙⣿⣿⣿⣿⣿⣿⣿⣿⣆
# ⠀⠀⠀⠀⢀⡀⠁⠀⠀⠈⠙⠛⠂⠈⣿⣿⣿⣿⣿⠿⡿⢿⣆
# ⠀⠀⠀⢀⡾⣁⣀⠀⠴⠂⠙⣗⡀⠀⢻⣿⣿⠭⢤⣴⣦⣤⣹⠀⠀⠀⢀⢴⣶⣆
# ⠀⠀⢀⣾⣿⣿⣿⣷⣮⣽⣾⣿⣥⣴⣿⣿⡿⢂⠔⢚⡿⢿⣿⣦⣴⣾⠸⣼⡿
# ⠀⢀⡞⠁⠙⠻⠿⠟⠉⠀⠛⢹⣿⣿⣿⣿⣿⣌⢤⣼⣿⣾⣿⡟⠉
# ⠀⣾⣷⣶⠇⠀⠀⣤⣄⣀⡀⠈⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇
# ⠀⠉⠈⠉⠀⠀⢦⡈⢻⣿⣿⣿⣶⣶⣶⣶⣤⣽⡹⣿⣿⣿⣿⡇
# ⠀⠀⠀⠀⠀⠀⠀⠉⠲⣽⡻⢿⣿⣿⣿⣿⣿⣿⣷⣜⣿⣿⣿⡇
# ⠀⠀⠀⠀⠀⢸⣿⣿⣷⣶⣮⣭⣽⣿⣿⣿⣿⣿⣿⣿⠇
# ⠀⠀⠀⠀⠀⠀⣀⣀⣈⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇
# ⠀⠀⠀⠀⠀⠀⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃
# https://steamcommunity.com/sharedfiles/filedetails/?id=2902061979
if data == null: failures.append("Data is null")
CoreTypes.ValidationType.IS_NORMALIZED:
# If not a Vector2, Vector3, Vector4, Plane or Quaternion, skip
match(typeof(data)):
Variant.Type.TYPE_VECTOR2: pass
Variant.Type.TYPE_VECTOR3: pass
Variant.Type.TYPE_VECTOR4: pass
Variant.Type.TYPE_PLANE: pass
Variant.Type.TYPE_QUATERNION: pass
_:
logger.warn("Can't determine if data is normalized as data is not of type Vector2, Vector3, Vector4, Plane or Quaternion")
continue
# Perform check
if !data.is_normalized(): failures.append("Data is not normalized")
CoreTypes.ValidationType.IS_ORTHONORMALIZED:
# If not a Transform2D, Transform3D, or Basis, skip
match(typeof(data)):
Variant.Type.TYPE_TRANSFORM2D: pass
Variant.Type.TYPE_TRANSFORM3D: pass
Variant.Type.TYPE_BASIS: pass
_:
logger.warn("Can't determine if data is orthonormalized as data is not of type Transform2D, Transform3D or Basis")
continue
# Perform check
if !data.is_orthonormalized(): failures.append("Data is not orthonormalized")
_: await logger.crash(core.stringify_variables("Invalid validation rule type %type%", { "type": rule["type"] }))
return failures.size() == 0
# +++ types and classes +++
## Validates if [param data] matches some [enum Variant.Type].
func matches_type(types: Array[Variant.Type]) -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.MATCHES_TYPE, "types": types })
return self
## Validates if [param data] matches some class.
func matches_class(clazz: StringName, exact: bool) -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.MATCHES_CLASS, "class": clazz, "exact": exact })
return self
# +++ ranges +++
## Validates if [param data] contains
func in_range_int(from: int, to: int) -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.IN_RANGE, "matched_against": "integer", "from": from, "to": to })
return self
func in_range_float(from: float, to: float) -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.IN_RANGE, "matched_against": "float", "from": from, "to": to })
return self
func has_minimum_value_int(minimum: int) -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.HAS_MINIMUM, "matched_against": "integer", "minimum": minimum })
return self
func has_maximum_value_int(maximum: int) -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.HAS_MAXIMUM, "matched_against": "integer", "maximum": maximum })
return self
func has_minimum_value_float(minimum: float) -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.HAS_MINIMUM, "matched_against": "float", "minimum": minimum })
return self
func has_maximum_value_float(maximum: float) -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.HAS_MAXIMUM, "matched_against": "float", "maximum": maximum })
return self
# +++ values +++
func has_values(values: Array, all_required: bool) -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.HAS_VALUES, "values": values, "all_required": all_required })
return self
func contains(values: Array[String], minimum_matches: int = 1) -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.HAS_VALUES, "values": values, "minimum_matches": minimum_matches })
return self
func matches_regex(regex_string: String) -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.MATCHES_REGEX, "regex_string": regex_string })
return self
# +++ empty/null and booleans +++
func is_not_empty() -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.IS_NOT_EMPTY })
return self
func is_not_null() -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.IS_NOT_NULL })
return self
func is_normalized() -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.IS_NORMALIZED })
return self
func is_orthonormalized() -> CoreValidationSingle:
rules.append({ "type": CoreTypes.ValidationType.IS_ORTHONORMALIZED })
return self

View file

@ -34,7 +34,7 @@ const version_fork: String = ""
# Modules
## Used internally for loading, managing and unloading modules.
const modules: Array[String] = [ "logger", "misc", "sms", "logui", "erm", "storage" ]
const modules: Array[String] = [ "logger", "misc", "sms", "logui", "erm", "storage", "validation" ]
## CORE's configuration object.[br]
## [b]NEVER access this yourself! To change the configuration use [method reload_configuration] instead.[/b]
var config: CoreConfiguration
@ -54,6 +54,8 @@ var logui: CoreBaseModule
var erm: CoreBaseModule
## The 'Storage' module
var storage: CoreBaseModule
## The 'Data Validation' module
var validation: CoreBaseModule
# /etc/
## Stores the path to CORE's installation directory.[br]

47
src/validation.gd Normal file
View file

@ -0,0 +1,47 @@
# CORE FRAMEWORK SOURCE FILE
# Copyright (c) 2024 The StarOpenSource Project & Contributors
# Licensed under the GNU Affero General Public License v3
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
## Allows for data validation.
extends CoreBaseModule
#var schemas: Dictionary
var singles: Array[CoreValidationSingle]
# +++ module +++
func _cleanup() -> void:
var singles_remove_enty: Array[CoreLoggerInstance] = []
for single in singles:
singles_remove_enty.append(single)
if !is_instance_valid(single): continue
if !is_instance_valid(single.parent):
logger.diag("Removing single '" + single.name + "'")
single.queue_free()
for single in singles_remove_enty:
singles.remove_at(singles.find(single))
func _schedule() -> void:
for single in singles:
if is_instance_valid(single):
logger.diag("Removing single '" + single.name + "'")
single.queue_free()
# +++ data validation +++
#func get_schema(Dictionary schema) -> CoreValidationSchema:
# return CoreValidationSchema.new(core, schema)
func get_single(data, parent) -> CoreValidationSingle:
singles.append(parent)
return CoreValidationSingle.new(core, data)