RapidGameFramework
Reusable Godot managers for data-driven small games
Loading...
Searching...
No Matches
data_validation_manager.gd
Go to the documentation of this file.
1extends RefCounted
2
3
7
8
9
10func load_json(file_path:String) -> Dictionary:
11 var file = FileAccess.open(file_path, FileAccess.READ)
12 if file == null:
13 return {"ok": false, "data": {}, "issues": [_issue("missing_file", file_path, "Unable to open JSON file.")]}
14 var parsed = JSON.parse_string(file.get_as_text())
15 if not parsed is Dictionary:
16 return {"ok": false, "data": {}, "issues": [_issue("invalid_json", file_path, "JSON root must be an object.")]}
17 return {"ok": true, "data": parsed, "issues": []}
18
19
20
21func validate_records(records:Array, schema:Dictionary = {}, path:String = "records") -> Array:
22 var issues := []
23 var id_field = str(schema.get("id_field", "id"))
24 var required = schema.get("required", []) if schema.get("required", []) is Array else []
25 var seen_ids := {}
26 for index in range(records.size()):
27 var record = records[index]
28 var record_path = "%s[%d]" % [path, index]
29 if not record is Dictionary:
30 issues.append(_issue("invalid_record", record_path, "Record must be an object."))
31 continue
32 for field in required:
33 if not record.has(str(field)) or str(record.get(str(field), "")).strip_edges() == "":
34 issues.append(_issue("missing_field", "%s.%s" % [record_path, str(field)], "Required field is missing."))
35 var id = str(record.get(id_field, ""))
36 if id != "":
37 if seen_ids.has(id):
38 issues.append(_issue("duplicate_id", record_path, "Duplicate %s '%s'." % [id_field, id]))
39 seen_ids[id] = true
40 _validate_references(record, schema.get("references", []), record_path, issues)
41 return issues
42
43
44
45func validate_section(data:Dictionary, section:String, schema:Dictionary = {}) -> Array:
46 var records = data.get(section, [])
47 if not records is Array:
48 return [_issue("invalid_section", section, "Section must be an array.")]
49 return validate_records(records, schema, section)
50
51
52
53func summarize(issues:Array) -> Dictionary:
54 var by_type := {}
55 for issue in issues:
56 if issue is Dictionary:
57 var type = str(issue.get("type", "unknown"))
58 by_type[type] = int(by_type.get(type, 0)) + 1
59 return {
60 "ok": issues.is_empty(),
61 "count": issues.size(),
62 "by_type": by_type
63 }
64
65
66
67func validate_bundle(bundle:Dictionary) -> Dictionary:
68 var all_issues := []
69 for file_config in bundle.get("files", []):
70 if not file_config is Dictionary:
71 continue
72 var path = str(file_config.get("path", ""))
73 var loaded = load_json(path)
74 all_issues.append_array(loaded.get("issues", []))
75 if not bool(loaded.get("ok", false)):
76 continue
77 var data = loaded.get("data", {})
78 for section_config in file_config.get("sections", []):
79 if not section_config is Dictionary:
80 continue
81 var section = str(section_config.get("name", ""))
82 all_issues.append_array(validate_section(data, section, section_config.get("schema", {})))
83 var summary = summarize(all_issues)
84 summary["issues"] = all_issues
85 return summary
86
87
88func _validate_references(record:Dictionary, references, record_path:String, issues:Array) -> void:
89 if not references is Array:
90 return
91 for reference in references:
92 if not reference is Dictionary:
93 continue
94 var field = str(reference.get("field", ""))
95 if field == "" or not record.has(field):
96 continue
97 var allowed = reference.get("allowed", [])
98 if not allowed is Array:
99 continue
100 var values = record.get(field)
101 var list = values if values is Array else [values]
102 for value in list:
103 var id = str(value)
104 if id != "" and not allowed.has(id):
105 issues.append(_issue("bad_reference", "%s.%s" % [record_path, field], "Unknown reference '%s'." % id))
106
107
108func _issue(type:String, path:String, message:String) -> Dictionary:
109 return {
110 "type": type,
111 "path": path,
112 "message": message
113 }