refactor(client): unify duplicate YAML parsers into YamlParser (#560)
Extract shared YamlParser utility (client/scripts/util/yaml_parser.gd) with parse() for nested typed dicts and parse_flat() for dotted-key string format. UIStrings._parse_yaml() and ChecklistEvaluator's inline parser both delegate to YamlParser, removing ~140 lines of duplication. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -49,44 +49,6 @@ func reload() -> void:
|
||||
|
||||
## Parse YAML with arbitrary nesting depth.
|
||||
## Returns flat Dictionary with dotted keys: { "section.sub.key": "value" }.
|
||||
## Delegates to YamlParser.parse_flat() (#560).
|
||||
static func _parse_yaml(text: String) -> Dictionary:
|
||||
var strings := {}
|
||||
var stack: Array = [] # [[indent, key], ...]
|
||||
for line in text.split("\n"):
|
||||
var stripped := line.strip_edges(false, true)
|
||||
if stripped.is_empty() or stripped.begins_with("#"):
|
||||
continue
|
||||
var indent := line.length() - line.lstrip(" ").length()
|
||||
var content := stripped.strip_edges()
|
||||
var colon_pos := content.find(":")
|
||||
if colon_pos < 0:
|
||||
continue
|
||||
var key := content.substr(0, colon_pos).strip_edges()
|
||||
var val := content.substr(colon_pos + 1).strip_edges()
|
||||
# Trailing comment without a value — treat as section header
|
||||
if val.begins_with("#"):
|
||||
val = ""
|
||||
# Pop sections at same or deeper indent
|
||||
while stack.size() > 0 and stack.back()[0] >= indent:
|
||||
stack.pop_back()
|
||||
if val.is_empty():
|
||||
# Section header — push onto stack
|
||||
stack.push_back([indent, key])
|
||||
else:
|
||||
# Leaf value — extract from quotes or strip inline comment
|
||||
if val.begins_with("\""):
|
||||
var end_quote := val.find("\"", 1)
|
||||
if end_quote > 0:
|
||||
val = val.substr(1, end_quote - 1)
|
||||
else:
|
||||
val = val.substr(1)
|
||||
else:
|
||||
var comment_pos := val.find(" #")
|
||||
if comment_pos >= 0:
|
||||
val = val.substr(0, comment_pos).strip_edges()
|
||||
var dotted_key := ""
|
||||
for entry in stack:
|
||||
dotted_key += entry[1] + "."
|
||||
dotted_key += key
|
||||
strings[dotted_key] = val
|
||||
return strings
|
||||
return YamlParser.parse_flat(text)
|
||||
|
||||
@@ -233,12 +233,7 @@ func _find_entity(entity_id: int) -> bool:
|
||||
return false
|
||||
|
||||
|
||||
# -- YAML parsing (checklist-specific) -----------------------------------------
|
||||
# Handles the constrained checklist YAML format: top-level key:value pairs,
|
||||
# a conditions array of flat dictionaries. No nested arrays or anchors.
|
||||
#
|
||||
# Limitation: unquoted values containing " #" are truncated at the comment marker.
|
||||
# Use quoted strings ("value # with hash") if values must contain literal hashes.
|
||||
# -- YAML parsing --------------------------------------------------------------
|
||||
|
||||
func _load_checklist_file(path: String) -> Dictionary:
|
||||
if not FileAccess.file_exists(path):
|
||||
@@ -252,103 +247,6 @@ func _load_checklist_file(path: String) -> Dictionary:
|
||||
return parse_checklist_yaml(text)
|
||||
|
||||
|
||||
## Delegates to YamlParser.parse() (#560).
|
||||
static func parse_checklist_yaml(text: String) -> Dictionary:
|
||||
var result := {}
|
||||
var conditions: Array = []
|
||||
var current_item: Dictionary = {}
|
||||
var in_conditions := false
|
||||
|
||||
for line in text.split("\n"):
|
||||
var stripped := line.strip_edges(false, true)
|
||||
if stripped.is_empty() or stripped.strip_edges().begins_with("#"):
|
||||
continue
|
||||
|
||||
var indent := line.length() - line.lstrip(" ").length()
|
||||
var content := stripped.strip_edges()
|
||||
|
||||
# Detect conditions: array header
|
||||
if content == "conditions:":
|
||||
in_conditions = true
|
||||
continue
|
||||
|
||||
if not in_conditions:
|
||||
# Top-level key: value
|
||||
var colon := content.find(":")
|
||||
if colon >= 0:
|
||||
var key := content.substr(0, colon).strip_edges()
|
||||
var val_str := content.substr(colon + 1).strip_edges()
|
||||
result[key] = _parse_value(val_str)
|
||||
else:
|
||||
if content.begins_with("- "):
|
||||
# New array item — flush previous
|
||||
if not current_item.is_empty():
|
||||
conditions.append(current_item)
|
||||
current_item = {}
|
||||
var rest := content.substr(2).strip_edges()
|
||||
var colon := rest.find(":")
|
||||
if colon >= 0:
|
||||
var key := rest.substr(0, colon).strip_edges()
|
||||
var val_str := rest.substr(colon + 1).strip_edges()
|
||||
current_item[key] = _parse_value(val_str)
|
||||
elif indent >= 2 and not current_item.is_empty():
|
||||
# Continuation of current array item
|
||||
var colon := content.find(":")
|
||||
if colon >= 0:
|
||||
var key := content.substr(0, colon).strip_edges()
|
||||
var val_str := content.substr(colon + 1).strip_edges()
|
||||
current_item[key] = _parse_value(val_str)
|
||||
elif indent == 0:
|
||||
# Back to top level — shouldn't happen in valid checklist YAML
|
||||
in_conditions = false
|
||||
if not current_item.is_empty():
|
||||
conditions.append(current_item)
|
||||
current_item = {}
|
||||
var colon := content.find(":")
|
||||
if colon >= 0:
|
||||
var key := content.substr(0, colon).strip_edges()
|
||||
var val_str := content.substr(colon + 1).strip_edges()
|
||||
result[key] = _parse_value(val_str)
|
||||
|
||||
# Flush last item
|
||||
if not current_item.is_empty():
|
||||
conditions.append(current_item)
|
||||
|
||||
if not conditions.is_empty():
|
||||
result["conditions"] = conditions
|
||||
|
||||
return result
|
||||
|
||||
|
||||
static func _parse_value(val: String) -> Variant:
|
||||
if val.is_empty():
|
||||
return ""
|
||||
|
||||
# Strip inline comments (not inside quotes)
|
||||
if not val.begins_with("\""):
|
||||
var comment_pos := val.find(" #")
|
||||
if comment_pos >= 0:
|
||||
val = val.substr(0, comment_pos).strip_edges()
|
||||
|
||||
# Quoted string
|
||||
if val.begins_with("\""):
|
||||
var end_quote := val.find("\"", 1)
|
||||
if end_quote > 0:
|
||||
return val.substr(1, end_quote - 1)
|
||||
return val.substr(1)
|
||||
|
||||
# Boolean
|
||||
if val == "true":
|
||||
return true
|
||||
if val == "false":
|
||||
return false
|
||||
|
||||
# Float (contains decimal point)
|
||||
if val.contains(".") and val.is_valid_float():
|
||||
return val.to_float()
|
||||
|
||||
# Integer
|
||||
if val.is_valid_int():
|
||||
return val.to_int()
|
||||
|
||||
# Plain string
|
||||
return val
|
||||
return YamlParser.parse(text)
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
class_name YamlParser
|
||||
## Shared YAML parser — common subset used by ui_strings.gd and checklist_evaluator.gd.
|
||||
##
|
||||
## Handles: nested sections (maps), arrays of dict items (- key: val), typed values.
|
||||
## Returns a hierarchical Dictionary. Use flatten() to convert to dotted-key format
|
||||
## (as UIStrings._parse_yaml() requires).
|
||||
##
|
||||
## Limitations: single-line values only; no YAML anchors/aliases; no flow syntax.
|
||||
## String values: quotes stripped. Booleans, ints, and floats are type-inferred.
|
||||
##
|
||||
## Spec ref: #560 (Sprint 20 — unify duplicate YAML parsers), D-030 (testability).
|
||||
|
||||
|
||||
## Parse YAML text into a hierarchical Dictionary.
|
||||
## Nested sections become nested dicts. Array items (- key: val) become Arrays.
|
||||
## Values are type-inferred: bool, int, float, or String.
|
||||
static func parse(text: String) -> Dictionary:
|
||||
var root: Dictionary = {}
|
||||
# Stack: [{indent: int, key: String}] — path of open section headers
|
||||
var stack: Array = []
|
||||
# Array state
|
||||
var current_array: Variant = null # Array being built, or null
|
||||
var current_item: Variant = null # Dict being built for current array item, or null
|
||||
var array_parent_indent: int = -1 # indent of the "key:" line that owns the array
|
||||
|
||||
for raw_line in text.split("\n"):
|
||||
var stripped := raw_line.strip_edges(false, true)
|
||||
if stripped.is_empty() or stripped.strip_edges().begins_with("#"):
|
||||
continue
|
||||
var indent: int = raw_line.length() - raw_line.lstrip(" ").length()
|
||||
var content: String = stripped.strip_edges()
|
||||
|
||||
# --- Array item (- key: value) ---
|
||||
if content.begins_with("- "):
|
||||
# First item: convert parent section's {} placeholder to []
|
||||
if current_array == null and stack.size() > 0:
|
||||
var parent := _node_at(root, stack, true)
|
||||
var arr_key: String = stack.back()["key"]
|
||||
var new_arr: Array = []
|
||||
parent[arr_key] = new_arr
|
||||
current_array = new_arr
|
||||
array_parent_indent = stack.back()["indent"]
|
||||
# Flush previous item and start a new one
|
||||
if current_item != null:
|
||||
current_array.append(current_item)
|
||||
current_item = {}
|
||||
var rest: String = content.substr(2).strip_edges()
|
||||
var colon: int = rest.find(":")
|
||||
if colon >= 0:
|
||||
var k: String = rest.substr(0, colon).strip_edges()
|
||||
var v: String = rest.substr(colon + 1).strip_edges()
|
||||
current_item[k] = _parse_value(v)
|
||||
continue
|
||||
|
||||
# --- Continuation line within current array item ---
|
||||
if current_array != null and indent > array_parent_indent:
|
||||
var colon: int = content.find(":")
|
||||
if colon >= 0 and current_item != null:
|
||||
var k: String = content.substr(0, colon).strip_edges()
|
||||
var v: String = content.substr(colon + 1).strip_edges()
|
||||
current_item[k] = _parse_value(v)
|
||||
continue
|
||||
|
||||
# --- End of array (indent has returned to array level or above) ---
|
||||
if current_array != null:
|
||||
if current_item != null:
|
||||
current_array.append(current_item)
|
||||
current_item = null
|
||||
current_array = null
|
||||
array_parent_indent = -1
|
||||
if stack.size() > 0:
|
||||
stack.pop_back() # pop the array-owning key
|
||||
|
||||
# --- Regular key: value or section header ---
|
||||
var colon: int = content.find(":")
|
||||
if colon < 0:
|
||||
continue
|
||||
var key: String = content.substr(0, colon).strip_edges()
|
||||
var val_str: String = content.substr(colon + 1).strip_edges()
|
||||
|
||||
# Pop sections at the same or deeper indent (we're back at a shallower level)
|
||||
while stack.size() > 0 and stack.back()["indent"] >= indent:
|
||||
stack.pop_back()
|
||||
|
||||
var node: Dictionary = _node_at(root, stack, false)
|
||||
|
||||
if val_str.is_empty() or val_str.begins_with("#"):
|
||||
# Section header — create nested dict (may become Array if - items follow)
|
||||
node[key] = {}
|
||||
stack.push_back({"indent": indent, "key": key})
|
||||
else:
|
||||
node[key] = _parse_value(val_str)
|
||||
|
||||
# Flush the last array item if the file ended inside an array
|
||||
if current_array != null and current_item != null:
|
||||
current_array.append(current_item)
|
||||
|
||||
return root
|
||||
|
||||
|
||||
## Convenience: parse text and flatten to dotted-key format in one call.
|
||||
## Used by UIStrings._parse_yaml() — equivalent to flatten(parse(text)).
|
||||
static func parse_flat(text: String) -> Dictionary:
|
||||
return flatten(parse(text))
|
||||
|
||||
|
||||
## Flatten a hierarchical dict to dotted-key format (for UIStrings compatibility).
|
||||
## {"a": {"b": "v"}} → {"a.b": "v"}
|
||||
## Arrays are skipped — dotted-key format does not represent them.
|
||||
## All values are converted to String (UIStrings stores display text, not typed data).
|
||||
static func flatten(d: Dictionary, prefix: String = "") -> Dictionary:
|
||||
var result: Dictionary = {}
|
||||
for k in d:
|
||||
var full_key: String = (prefix + "." if not prefix.is_empty() else "") + str(k)
|
||||
var v = d[k]
|
||||
if v is Dictionary:
|
||||
result.merge(flatten(v, full_key))
|
||||
elif not v is Array:
|
||||
result[full_key] = str(v)
|
||||
return result
|
||||
|
||||
|
||||
## Parse a single YAML value string into a typed GDScript value.
|
||||
## Strips inline comments, handles quoted strings, infers bool/int/float/String.
|
||||
static func _parse_value(val: String) -> Variant:
|
||||
if val.is_empty():
|
||||
return ""
|
||||
# Strip inline comment outside quotes
|
||||
if not val.begins_with("\""):
|
||||
var comment_pos: int = val.find(" #")
|
||||
if comment_pos >= 0:
|
||||
val = val.substr(0, comment_pos).strip_edges()
|
||||
# Quoted string — extract content between quotes
|
||||
if val.begins_with("\""):
|
||||
var end_quote: int = val.find("\"", 1)
|
||||
if end_quote > 0:
|
||||
return val.substr(1, end_quote - 1)
|
||||
return val.substr(1)
|
||||
# Boolean
|
||||
if val == "true": return true
|
||||
if val == "false": return false
|
||||
# Float (must have decimal point)
|
||||
if val.contains(".") and val.is_valid_float():
|
||||
return val.to_float()
|
||||
# Integer
|
||||
if val.is_valid_int():
|
||||
return val.to_int()
|
||||
# Plain string
|
||||
return val
|
||||
|
||||
|
||||
## Navigate root following the stack key path.
|
||||
## parent=true: navigate one level less (returns the parent node, not the leaf).
|
||||
static func _node_at(root: Dictionary, stack: Array, parent: bool) -> Dictionary:
|
||||
var node: Dictionary = root
|
||||
var depth: int = stack.size() - (1 if parent else 0)
|
||||
for i in range(depth):
|
||||
var k: String = stack[i]["key"]
|
||||
if node.has(k) and node[k] is Dictionary:
|
||||
node = node[k]
|
||||
else:
|
||||
break
|
||||
return node
|
||||
@@ -0,0 +1,261 @@
|
||||
## YamlParser unit tests (#560).
|
||||
## Verifies parse() (nested, typed) and parse_flat() (dotted keys, string values).
|
||||
## Covers: maps, nested maps, arrays of dicts, type conversion, comments,
|
||||
## quoted strings, empty input, edge cases.
|
||||
##
|
||||
## D-030: fixture-based, server-free, no subprocess required.
|
||||
class_name TestYamlParser
|
||||
extends GdUnitTestSuite
|
||||
|
||||
|
||||
# -- parse(): basic key-value pairs -------------------------------------------
|
||||
|
||||
func test_parse_simple_key_value() -> void:
|
||||
var result := YamlParser.parse("key: value")
|
||||
assert_that(result["key"]).is_equal("value")
|
||||
|
||||
|
||||
func test_parse_quoted_string() -> void:
|
||||
var result := YamlParser.parse('key: "hello world"')
|
||||
assert_that(result["key"]).is_equal("hello world")
|
||||
|
||||
|
||||
func test_parse_empty_quoted_string() -> void:
|
||||
var result := YamlParser.parse('key: ""')
|
||||
assert_that(result["key"]).is_equal("")
|
||||
|
||||
|
||||
func test_parse_integer_value() -> void:
|
||||
var result := YamlParser.parse("count: 42")
|
||||
assert_that(result["count"]).is_equal(42)
|
||||
assert_that(typeof(result["count"])).is_equal(TYPE_INT)
|
||||
|
||||
|
||||
func test_parse_negative_integer() -> void:
|
||||
var result := YamlParser.parse("offset: -3")
|
||||
assert_that(result["offset"]).is_equal(-3)
|
||||
|
||||
|
||||
func test_parse_float_value() -> void:
|
||||
var result := YamlParser.parse("radius: 2.5")
|
||||
assert_that(typeof(result["radius"])).is_equal(TYPE_FLOAT)
|
||||
assert_float(result["radius"]).is_equal_approx(2.5, 0.001)
|
||||
|
||||
|
||||
func test_parse_boolean_true() -> void:
|
||||
var result := YamlParser.parse("enabled: true")
|
||||
assert_that(result["enabled"]).is_equal(true)
|
||||
assert_that(typeof(result["enabled"])).is_equal(TYPE_BOOL)
|
||||
|
||||
|
||||
func test_parse_boolean_false() -> void:
|
||||
var result := YamlParser.parse("enabled: false")
|
||||
assert_that(result["enabled"]).is_equal(false)
|
||||
|
||||
|
||||
func test_parse_empty_input() -> void:
|
||||
var result := YamlParser.parse("")
|
||||
assert_that(result.size()).is_equal(0)
|
||||
|
||||
|
||||
func test_parse_only_comments() -> void:
|
||||
var result := YamlParser.parse("# comment\n# another")
|
||||
assert_that(result.size()).is_equal(0)
|
||||
|
||||
|
||||
func test_parse_inline_comment_stripped() -> void:
|
||||
var result := YamlParser.parse("room_id: test # this is a comment")
|
||||
assert_that(result["room_id"]).is_equal("test")
|
||||
|
||||
|
||||
func test_parse_quoted_value_with_hash() -> void:
|
||||
## Quoted strings preserve literal # characters.
|
||||
var result := YamlParser.parse('color: "#e0e8ff"')
|
||||
assert_that(result["color"]).is_equal("#e0e8ff")
|
||||
|
||||
|
||||
func test_parse_value_with_colon() -> void:
|
||||
var result := YamlParser.parse('time: "12:30"')
|
||||
assert_that(result["time"]).is_equal("12:30")
|
||||
|
||||
|
||||
# -- parse(): nested maps -----------------------------------------------------
|
||||
|
||||
func test_parse_nested_map() -> void:
|
||||
var yaml := "section:\n key: value"
|
||||
var result := YamlParser.parse(yaml)
|
||||
assert_that(result.has("section")).is_true()
|
||||
assert_that(result["section"] is Dictionary).is_true()
|
||||
assert_that(result["section"]["key"]).is_equal("value")
|
||||
|
||||
|
||||
func test_parse_deeply_nested() -> void:
|
||||
var yaml := "a:\n b:\n c: deep"
|
||||
var result := YamlParser.parse(yaml)
|
||||
assert_that(result["a"]["b"]["c"]).is_equal("deep")
|
||||
|
||||
|
||||
func test_parse_multiple_sections() -> void:
|
||||
var yaml := "hud:\n mode: Mode\ninteraction:\n talk: Talk"
|
||||
var result := YamlParser.parse(yaml)
|
||||
assert_that(result["hud"]["mode"]).is_equal("Mode")
|
||||
assert_that(result["interaction"]["talk"]).is_equal("Talk")
|
||||
|
||||
|
||||
func test_parse_multiple_keys_per_section() -> void:
|
||||
var yaml := "hud:\n a: 1\n b: 2\n c: 3"
|
||||
var result := YamlParser.parse(yaml)
|
||||
assert_that(result["hud"]["a"]).is_equal(1)
|
||||
assert_that(result["hud"]["b"]).is_equal(2)
|
||||
assert_that(result["hud"]["c"]).is_equal(3)
|
||||
|
||||
|
||||
func test_parse_sibling_subsections() -> void:
|
||||
var yaml := "states:\n a:\n x: 1\n b:\n x: 2"
|
||||
var result := YamlParser.parse(yaml)
|
||||
assert_that(result["states"]["a"]["x"]).is_equal(1)
|
||||
assert_that(result["states"]["b"]["x"]).is_equal(2)
|
||||
|
||||
|
||||
func test_parse_section_with_trailing_comment() -> void:
|
||||
## "section: # comment" should be treated as a section header.
|
||||
var yaml := "section: # comment\n key: value"
|
||||
var result := YamlParser.parse(yaml)
|
||||
assert_that(result["section"]["key"]).is_equal("value")
|
||||
|
||||
|
||||
# -- parse(): arrays of dicts -------------------------------------------------
|
||||
|
||||
func test_parse_single_array_item() -> void:
|
||||
var yaml := "conditions:\n - id: test-1\n type: near\n x: 10"
|
||||
var result := YamlParser.parse(yaml)
|
||||
assert_that(result.has("conditions")).is_true()
|
||||
assert_that(result["conditions"] is Array).is_true()
|
||||
assert_that(result["conditions"].size()).is_equal(1)
|
||||
assert_that(result["conditions"][0]["id"]).is_equal("test-1")
|
||||
assert_that(result["conditions"][0]["type"]).is_equal("near")
|
||||
assert_that(result["conditions"][0]["x"]).is_equal(10)
|
||||
|
||||
|
||||
func test_parse_multiple_array_items() -> void:
|
||||
var yaml := "conditions:\n - id: a\n x: 1\n - id: b\n x: 2"
|
||||
var result := YamlParser.parse(yaml)
|
||||
assert_that(result["conditions"].size()).is_equal(2)
|
||||
assert_that(result["conditions"][0]["id"]).is_equal("a")
|
||||
assert_that(result["conditions"][1]["id"]).is_equal("b")
|
||||
|
||||
|
||||
func test_parse_array_items_with_blank_lines() -> void:
|
||||
var yaml := "conditions:\n - id: a\n x: 1\n\n - id: b\n x: 2"
|
||||
var result := YamlParser.parse(yaml)
|
||||
assert_that(result["conditions"].size()).is_equal(2)
|
||||
|
||||
|
||||
func test_parse_top_level_plus_array() -> void:
|
||||
## Checklist format: top-level key-value pairs followed by a conditions array.
|
||||
var yaml := "room_id: warehouse\nconditions:\n - id: c1\n type: near\n x: 5"
|
||||
var result := YamlParser.parse(yaml)
|
||||
assert_that(result["room_id"]).is_equal("warehouse")
|
||||
assert_that(result["conditions"].size()).is_equal(1)
|
||||
assert_that(result["conditions"][0]["id"]).is_equal("c1")
|
||||
|
||||
|
||||
func test_parse_array_typed_values() -> void:
|
||||
var yaml := "items:\n - id: t\n x: 42\n radius: 2.5\n active: true"
|
||||
var result := YamlParser.parse(yaml)
|
||||
var item: Dictionary = result["items"][0]
|
||||
assert_that(item["x"]).is_equal(42)
|
||||
assert_that(typeof(item["radius"])).is_equal(TYPE_FLOAT)
|
||||
assert_that(item["active"]).is_equal(true)
|
||||
|
||||
|
||||
# -- parse_flat(): dotted keys -------------------------------------------------
|
||||
|
||||
func test_flat_simple() -> void:
|
||||
var yaml := "section:\n key: value"
|
||||
var result := YamlParser.parse_flat(yaml)
|
||||
assert_that(result.has("section.key")).is_true()
|
||||
assert_that(result["section.key"]).is_equal("value")
|
||||
|
||||
|
||||
func test_flat_deeply_nested() -> void:
|
||||
var yaml := "a:\n b:\n c: deep"
|
||||
var result := YamlParser.parse_flat(yaml)
|
||||
assert_that(result["a.b.c"]).is_equal("deep")
|
||||
|
||||
|
||||
func test_flat_multiple_sections() -> void:
|
||||
var yaml := "hud:\n mode: Mode\ninteraction:\n talk: Talk"
|
||||
var result := YamlParser.parse_flat(yaml)
|
||||
assert_that(result["hud.mode"]).is_equal("Mode")
|
||||
assert_that(result["interaction.talk"]).is_equal("Talk")
|
||||
|
||||
|
||||
func test_flat_values_are_strings() -> void:
|
||||
## parse_flat returns all values as strings, unlike parse() which returns typed.
|
||||
var yaml := "section:\n count: 42\n rate: 0.9\n active: true"
|
||||
var result := YamlParser.parse_flat(yaml)
|
||||
assert_that(result["section.count"]).is_equal("42")
|
||||
assert_that(result["section.rate"]).is_equal("0.9")
|
||||
assert_that(result["section.active"]).is_equal("true")
|
||||
|
||||
|
||||
func test_flat_quoted_value() -> void:
|
||||
var yaml := 'section:\n key: "hello world"'
|
||||
var result := YamlParser.parse_flat(yaml)
|
||||
assert_that(result["section.key"]).is_equal("hello world")
|
||||
|
||||
|
||||
func test_flat_inline_comment() -> void:
|
||||
var yaml := "hud:\n mode: Standard # default"
|
||||
var result := YamlParser.parse_flat(yaml)
|
||||
assert_that(result["hud.mode"]).is_equal("Standard")
|
||||
|
||||
|
||||
func test_flat_empty_quoted_value() -> void:
|
||||
var yaml := 'hud:\n prefix: "" # No prefix'
|
||||
var result := YamlParser.parse_flat(yaml)
|
||||
assert_that(result.has("hud.prefix")).is_true()
|
||||
assert_that(result["hud.prefix"]).is_equal("")
|
||||
|
||||
|
||||
func test_flat_skips_arrays() -> void:
|
||||
## Arrays have no dotted-key representation — they are skipped in flat output.
|
||||
var yaml := "room_id: test\nconditions:\n - id: c1\n x: 5"
|
||||
var result := YamlParser.parse_flat(yaml)
|
||||
assert_that(result.has("room_id")).is_true()
|
||||
# No dotted keys for array contents
|
||||
assert_that(result.has("conditions")).is_false()
|
||||
assert_that(result.has("conditions.0")).is_false()
|
||||
|
||||
|
||||
func test_flat_empty_input() -> void:
|
||||
var result := YamlParser.parse_flat("")
|
||||
assert_that(result.size()).is_equal(0)
|
||||
|
||||
|
||||
# -- Dialogue theme format (regression) ----------------------------------------
|
||||
|
||||
func test_dialogue_theme_format() -> void:
|
||||
## dialogue-theme.yaml has top-level values and one nested map (npc_colors).
|
||||
var yaml := "player_color: \"#e0e8ff\"\nnpc_colors:\n 0: \"#4a9ebb\"\n 1: \"#6bc9a6\"\npassive_opacity: 0.9"
|
||||
var flat := YamlParser.parse_flat(yaml)
|
||||
assert_that(flat["player_color"]).is_equal("#e0e8ff")
|
||||
assert_that(flat["npc_colors.0"]).is_equal("#4a9ebb")
|
||||
assert_that(flat["npc_colors.1"]).is_equal("#6bc9a6")
|
||||
assert_that(flat["passive_opacity"]).is_equal("0.9")
|
||||
|
||||
|
||||
# -- Checklist format (regression) ---------------------------------------------
|
||||
|
||||
func test_checklist_format() -> void:
|
||||
## checklist.yaml: top-level kv + conditions array with typed values.
|
||||
var yaml := "room_id: inventory_warehouse\nconditions:\n - id: test-1\n condition_type: player_near\n x: 10\n y: 20\n radius: 3.0"
|
||||
var result := YamlParser.parse(yaml)
|
||||
assert_that(result["room_id"]).is_equal("inventory_warehouse")
|
||||
var cond: Dictionary = result["conditions"][0]
|
||||
assert_that(cond["id"]).is_equal("test-1")
|
||||
assert_that(cond["condition_type"]).is_equal("player_near")
|
||||
assert_that(cond["x"]).is_equal(10)
|
||||
assert_that(cond["y"]).is_equal(20)
|
||||
assert_that(typeof(cond["radius"])).is_equal(TYPE_FLOAT)
|
||||
Reference in New Issue
Block a user