From b19128d7122b3c83122a7dcd9b1f80b11a18f8da Mon Sep 17 00:00:00 2001 From: Thorek <75499293+sndth@users.noreply.github.com> Date: Sun, 16 Aug 2026 19:34:32 +0200 Subject: [PATCH 1/3] Support empty keys in block and flow mappings An empty key was a special case at the document root only, where it produced an entry with an empty string key. Everywhere else a key separator without a preceding key was rejected, and in a flow mapping it was ignored, which dropped the entry: ```yaml key: value : empty key # parse_error: Detected invalid indentation - : foo # parse_error: mapping key should not be empty { key: value, : empty key } # parse_error: The ":" mapping value indicator must be # followed after a mapping key ``` Parse an empty key as a null node, as the YAML specification does, and add the entry wherever a key separator is found with no key before it: at the start of a line in a block mapping, in a block sequence entry, and in a flow mapping. The value of an empty key can be omitted as well, so add_empty_key_entry() also moves back to the parent mapping when the following token is not indented deeper, the same way an omitted value is handled for a normal key. This changes the type of an empty key from an empty string to a null node, which is a breaking change for anyone reading such an entry as node[""]. A quoted empty key ("": value) is unaffected and remains a string. Spec example 8.18 (test suite case S3PD) and the empty key cases NKF9 and PW8X of the yaml-test-suite now parse. CFD4 (an empty key in a single pair flow sequence) is still rejected: it needs single pair flow mappings, which are not supported for non-empty keys either ([foo: 1] is rejected as well). --- include/fkYAML/detail/input/deserializer.hpp | 69 ++++++++++++++++++-- single_include/fkYAML/node.hpp | 69 ++++++++++++++++++-- tests/unit_test/test_deserializer_class.cpp | 55 ++++++++++++++-- 3 files changed, 172 insertions(+), 21 deletions(-) diff --git a/include/fkYAML/detail/input/deserializer.hpp b/include/fkYAML/detail/input/deserializer.hpp index 7a68679d..14b92f8b 100644 --- a/include/fkYAML/detail/input/deserializer.hpp +++ b/include/fkYAML/detail/input/deserializer.hpp @@ -253,10 +253,7 @@ class basic_deserializer { apply_node_properties(root); m_context_stack.emplace_back( lexer.get_lines_processed(), lexer.get_last_token_begin_pos(), context_state_t::BLOCK_MAPPING, &root); - add_new_key(basic_node_type(""), line, indent); - token = lexer.get_next_token(); - line = lexer.get_lines_processed(); - indent = lexer.get_last_token_begin_pos(); + add_empty_key_entry(lexer, token, line, indent); break; case lexical_token_t::BLOCK_LITERAL_SCALAR: case lexical_token_t::BLOCK_FOLDED_SCALAR: @@ -500,15 +497,32 @@ class basic_deserializer { if FK_YAML_UNLIKELY (m_context_stack.empty()) { throw parse_error("A key separator is not allowed in this context.", line, indent); } - if FK_YAML_UNLIKELY (m_context_stack.back().state == context_state_t::BLOCK_SEQUENCE_ENTRY) { - // empty mapping keys are not supported. + if (m_context_stack.back().state == context_state_t::BLOCK_SEQUENCE_ENTRY) { + // The entry is a mapping whose first key is empty. // ```yaml // - : foo + // # -> [{null: foo}] // ``` - throw parse_error("mapping key should not be empty.", line, indent); + *mp_current_node = basic_node_type::mapping(); + apply_directive_set(*mp_current_node); + m_context_stack.emplace_back(line, indent, context_state_t::BLOCK_MAPPING, mp_current_node); + add_new_key(basic_node_type(), line, indent); + + token = lexer.get_next_token(); + indent = lexer.get_last_token_begin_pos(); + line = lexer.get_lines_processed(); + continue; } if (m_flow_context_depth > 0) { + if (m_context_stack.back().state != context_state_t::MAPPING_VALUE) { + // No key precedes this separator, so the entry has an empty key. + // ```yaml + // { : foo } + // # -> {null: foo} + // ``` + add_new_key(basic_node_type(), line, indent); + } break; } @@ -1281,6 +1295,18 @@ class basic_deserializer { } if (m_context_stack.back().state != context_state_t::BLOCK_MAPPING_EXPLICIT_KEY) { + const parse_context& cur_context = m_context_stack.back(); + if (cur_context.state == context_state_t::BLOCK_MAPPING && cur_context.indent == indent) { + // A key separator which begins a line belongs to an entry with an empty key. + // ```yaml + // foo: bar + // : baz + // # -> {foo: bar, null: baz} + // ``` + add_empty_key_entry(lexer, token, line, indent); + return; + } + pop_to_parent_node(line, indent, [indent](const parse_context& c) { return c.state == context_state_t::BLOCK_MAPPING_EXPLICIT_KEY && indent == c.indent; }); @@ -1394,6 +1420,35 @@ class basic_deserializer { return m_context_stack.back(); } + /// @brief Adds a mapping entry whose key is empty and moves to the token which follows it. + /// @note + /// An empty key is a null node. Its value can be omitted as well, in which case the following token + /// belongs to the parent mapping rather than to this entry. + /// ```yaml + /// : + /// foo: bar + /// # -> {null: null, foo: bar} + /// ``` + /// @param lexer The lexical analyzer to be used. + /// @param token The storage for the token which follows the key separator. + /// @param line The line of the key separator. Updated to the line of the following token. + /// @param indent The indentation width of the key separator. Updated for the following token. + void add_empty_key_entry(lexer_type& lexer, lexical_token& token, uint32_t& line, uint32_t& indent) { + const uint32_t key_line = line; + const uint32_t key_indent = indent; + add_new_key(basic_node_type(), line, indent); + + token = lexer.get_next_token(); + line = lexer.get_lines_processed(); + indent = lexer.get_last_token_begin_pos(); + + if (line > key_line && indent <= key_indent) { + pop_to_parent_node(line, indent, [key_indent](const parse_context& c) { + return c.state == context_state_t::BLOCK_MAPPING && key_indent == c.indent; + }); + } + } + /// @brief Adds an entry for an explicit key and makes its value node the current node. /// @note The current context must be the context of the explicit key. /// @param line The line where the value of the explicit key begins. diff --git a/single_include/fkYAML/node.hpp b/single_include/fkYAML/node.hpp index f7cac404..d74eb492 100644 --- a/single_include/fkYAML/node.hpp +++ b/single_include/fkYAML/node.hpp @@ -7694,10 +7694,7 @@ class basic_deserializer { apply_node_properties(root); m_context_stack.emplace_back( lexer.get_lines_processed(), lexer.get_last_token_begin_pos(), context_state_t::BLOCK_MAPPING, &root); - add_new_key(basic_node_type(""), line, indent); - token = lexer.get_next_token(); - line = lexer.get_lines_processed(); - indent = lexer.get_last_token_begin_pos(); + add_empty_key_entry(lexer, token, line, indent); break; case lexical_token_t::BLOCK_LITERAL_SCALAR: case lexical_token_t::BLOCK_FOLDED_SCALAR: @@ -7941,15 +7938,32 @@ class basic_deserializer { if FK_YAML_UNLIKELY (m_context_stack.empty()) { throw parse_error("A key separator is not allowed in this context.", line, indent); } - if FK_YAML_UNLIKELY (m_context_stack.back().state == context_state_t::BLOCK_SEQUENCE_ENTRY) { - // empty mapping keys are not supported. + if (m_context_stack.back().state == context_state_t::BLOCK_SEQUENCE_ENTRY) { + // The entry is a mapping whose first key is empty. // ```yaml // - : foo + // # -> [{null: foo}] // ``` - throw parse_error("mapping key should not be empty.", line, indent); + *mp_current_node = basic_node_type::mapping(); + apply_directive_set(*mp_current_node); + m_context_stack.emplace_back(line, indent, context_state_t::BLOCK_MAPPING, mp_current_node); + add_new_key(basic_node_type(), line, indent); + + token = lexer.get_next_token(); + indent = lexer.get_last_token_begin_pos(); + line = lexer.get_lines_processed(); + continue; } if (m_flow_context_depth > 0) { + if (m_context_stack.back().state != context_state_t::MAPPING_VALUE) { + // No key precedes this separator, so the entry has an empty key. + // ```yaml + // { : foo } + // # -> {null: foo} + // ``` + add_new_key(basic_node_type(), line, indent); + } break; } @@ -8722,6 +8736,18 @@ class basic_deserializer { } if (m_context_stack.back().state != context_state_t::BLOCK_MAPPING_EXPLICIT_KEY) { + const parse_context& cur_context = m_context_stack.back(); + if (cur_context.state == context_state_t::BLOCK_MAPPING && cur_context.indent == indent) { + // A key separator which begins a line belongs to an entry with an empty key. + // ```yaml + // foo: bar + // : baz + // # -> {foo: bar, null: baz} + // ``` + add_empty_key_entry(lexer, token, line, indent); + return; + } + pop_to_parent_node(line, indent, [indent](const parse_context& c) { return c.state == context_state_t::BLOCK_MAPPING_EXPLICIT_KEY && indent == c.indent; }); @@ -8835,6 +8861,35 @@ class basic_deserializer { return m_context_stack.back(); } + /// @brief Adds a mapping entry whose key is empty and moves to the token which follows it. + /// @note + /// An empty key is a null node. Its value can be omitted as well, in which case the following token + /// belongs to the parent mapping rather than to this entry. + /// ```yaml + /// : + /// foo: bar + /// # -> {null: null, foo: bar} + /// ``` + /// @param lexer The lexical analyzer to be used. + /// @param token The storage for the token which follows the key separator. + /// @param line The line of the key separator. Updated to the line of the following token. + /// @param indent The indentation width of the key separator. Updated for the following token. + void add_empty_key_entry(lexer_type& lexer, lexical_token& token, uint32_t& line, uint32_t& indent) { + const uint32_t key_line = line; + const uint32_t key_indent = indent; + add_new_key(basic_node_type(), line, indent); + + token = lexer.get_next_token(); + line = lexer.get_lines_processed(); + indent = lexer.get_last_token_begin_pos(); + + if (line > key_line && indent <= key_indent) { + pop_to_parent_node(line, indent, [key_indent](const parse_context& c) { + return c.state == context_state_t::BLOCK_MAPPING && key_indent == c.indent; + }); + } + } + /// @brief Adds an entry for an explicit key and makes its value node the current node. /// @note The current context must be the context of the explicit key. /// @param line The line where the value of the explicit key begins. diff --git a/tests/unit_test/test_deserializer_class.cpp b/tests/unit_test/test_deserializer_class.cpp index 489f3b87..3432cc07 100644 --- a/tests/unit_test/test_deserializer_class.cpp +++ b/tests/unit_test/test_deserializer_class.cpp @@ -33,19 +33,60 @@ TEST_CASE("Deserializer_KeySeparator") { REQUIRE(root.size() == 1); } - SUBCASE("empty mapping key in block sequences is unsupported") { - auto input_str = GENERATE(std::string("- : foo"), std::string("- - : foo")); - REQUIRE_THROWS_AS( - root = deserializer.deserialize(fkyaml::detail::input_adapter(input_str)), fkyaml::parse_error); + SUBCASE("empty mapping key in a block sequence entry") { + REQUIRE_NOTHROW(root = deserializer.deserialize(fkyaml::detail::input_adapter("- : foo"))); + REQUIRE(root.is_sequence()); + REQUIRE(root.size() == 1); + + fkyaml::node& entry_node = root[0]; + REQUIRE(entry_node.is_mapping()); + REQUIRE(entry_node.size() == 1); + REQUIRE(entry_node.contains(nullptr)); + REQUIRE(entry_node[nullptr].as_str() == "foo"); + } + + SUBCASE("empty mapping key in a nested block sequence entry") { + REQUIRE_NOTHROW(root = deserializer.deserialize(fkyaml::detail::input_adapter("- - : foo"))); + REQUIRE(root.is_sequence()); + REQUIRE(root.size() == 1); + REQUIRE(root[0].is_sequence()); + REQUIRE(root[0].size() == 1); + + fkyaml::node& entry_node = root[0][0]; + REQUIRE(entry_node.is_mapping()); + REQUIRE(entry_node.contains(nullptr)); + REQUIRE(entry_node[nullptr].as_str() == "foo"); } SUBCASE("empty root mapping key") { REQUIRE_NOTHROW(root = deserializer.deserialize(fkyaml::detail::input_adapter(": foo"))); REQUIRE(root.is_mapping()); REQUIRE(root.size() == 1); - REQUIRE(root.contains("")); - REQUIRE(root[""].is_string()); - REQUIRE(root[""].as_str() == "foo"); + REQUIRE(root.contains(nullptr)); + REQUIRE(root[nullptr].is_string()); + REQUIRE(root[nullptr].as_str() == "foo"); + } + + SUBCASE("empty mapping key after a normal entry") { + std::string input = "key: value\n" + ": empty key\n"; + REQUIRE_NOTHROW(root = deserializer.deserialize(fkyaml::detail::input_adapter(input))); + REQUIRE(root.is_mapping()); + REQUIRE(root.size() == 2); + REQUIRE(root.contains("key")); + REQUIRE(root["key"].as_str() == "value"); + REQUIRE(root.contains(nullptr)); + REQUIRE(root[nullptr].as_str() == "empty key"); + } + + SUBCASE("empty mapping key in a flow mapping") { + REQUIRE_NOTHROW(root = deserializer.deserialize(fkyaml::detail::input_adapter("{key: value, : empty key}"))); + REQUIRE(root.is_mapping()); + REQUIRE(root.size() == 2); + REQUIRE(root.contains("key")); + REQUIRE(root["key"].as_str() == "value"); + REQUIRE(root.contains(nullptr)); + REQUIRE(root[nullptr].as_str() == "empty key"); } SUBCASE("invalid explicit mapping key separator after root scalar") { From 1e9bf92aecba8cf17b80400092d1232177b0cd7e Mon Sep 17 00:00:00 2001 From: Thorek <75499293+sndth@users.noreply.github.com> Date: Tue, 18 Aug 2026 12:43:49 +0200 Subject: [PATCH 2/3] Parse a key separator which begins the contents of an explicit key The contents of an explicit key may themselves be a mapping entry with an empty key, as pointed out in #557: `? ` introduces an explicit key, and the `:` which follows it on the same line belongs to that key rather than to the entry the key is part of. Handle a key separator which begins the contents of an explicit key by turning the key node into a mapping and adding an entry with an empty key to it, the same way a scalar key does. The key separator which later terminates the explicit key can now find its context below the ones its own contents left on the stack, and completing a pending explicit key at the end of a document unwinds those contexts too. | input | before | after | |---|---|---| | `? :` | `{null: null}` | `{{null: null}: null}` | | `? : foo` | `{foo: null}` | `{{null: foo}: null}` | | `? foo: bar` | `{}` | `{{foo: bar}: null}` | | `? :` / `: baz` | parse_error | `{{null: null}: baz}` | The third row was silently dropping the whole entry before, and the fourth was rejected outright; both are the same shape as the case this issue is about. --- include/fkYAML/detail/input/deserializer.hpp | 55 ++++++++++++++++++-- single_include/fkYAML/node.hpp | 55 ++++++++++++++++++-- tests/unit_test/test_deserializer_class.cpp | 48 +++++++++++++++++ 3 files changed, 148 insertions(+), 10 deletions(-) diff --git a/include/fkYAML/detail/input/deserializer.hpp b/include/fkYAML/detail/input/deserializer.hpp index 14b92f8b..50d49dea 100644 --- a/include/fkYAML/detail/input/deserializer.hpp +++ b/include/fkYAML/detail/input/deserializer.hpp @@ -296,12 +296,20 @@ class basic_deserializer { last_type == lexical_token_t::END_OF_BUFFER || last_type == lexical_token_t::END_OF_DIRECTIVES || last_type == lexical_token_t::END_OF_DOCUMENT); - // An explicit key at the end of a document has no value either. + // An explicit key at the end of a document has no value either. Its own contents may have left + // more contexts on the stack, so those are unwound first. // ```yaml // ? foo - // # -> {foo: null} + // ? bar: baz + // # -> {foo: null, {bar: baz}: null} // ``` - add_explicit_key_with_null_value(); + while (!m_context_stack.empty()) { + if (m_context_stack.back().state == context_state_t::BLOCK_MAPPING_EXPLICIT_KEY) { + add_explicit_key_with_null_value(); + continue; + } + m_context_stack.pop_back(); + } // reset parameters for the next call. mp_current_node = nullptr; @@ -514,6 +522,26 @@ class basic_deserializer { continue; } + { + const parse_context& cur_context = m_context_stack.back(); + const bool is_explicit_key_content = + cur_context.state == context_state_t::BLOCK_MAPPING_EXPLICIT_KEY && cur_context.line == line; + if (is_explicit_key_content) { + // The contents of an explicit key begin with a key separator, so the key is a mapping + // whose first entry has an empty key. Whether that entry has a value, and whether the + // explicit key itself has one, is not known yet. + // ```yaml + // ? : foo + // # ^ this key separator begins the contents of the explicit key + // ``` + *mp_current_node = basic_node_type::mapping(); + apply_directive_set(*mp_current_node); + m_context_stack.emplace_back(line, indent, context_state_t::BLOCK_MAPPING, mp_current_node); + add_empty_key_entry(lexer, token, line, indent); + continue; + } + } + if (m_flow_context_depth > 0) { if (m_context_stack.back().state != context_state_t::MAPPING_VALUE) { // No key precedes this separator, so the entry has an empty key. @@ -676,8 +704,25 @@ class basic_deserializer { } // handle explicit mapping key separators. - if FK_YAML_UNLIKELY (m_context_stack.back().state != context_state_t::BLOCK_MAPPING_EXPLICIT_KEY) { - throw parse_error("Unexpected explicit mapping key separator is found.", line, indent); + if (m_context_stack.back().state != context_state_t::BLOCK_MAPPING_EXPLICIT_KEY) { + // The contents of the explicit key may have left their own contexts on the stack. + // ```yaml + // ? : + // : v + // # -> {{null: null}: v} + // ``` + // old_indent is the position of this key separator, while indent already refers to the + // token which follows it. + const auto is_key_context = [old_indent](const parse_context& c) { + return c.state == context_state_t::BLOCK_MAPPING_EXPLICIT_KEY && old_indent == c.indent; + }; + const bool has_key_context = + std::any_of(m_context_stack.rbegin(), m_context_stack.rend(), is_key_context); + if FK_YAML_UNLIKELY (!has_key_context) { + throw parse_error("Unexpected explicit mapping key separator is found.", line, indent); + } + + pop_to_parent_node(old_line, old_indent, is_key_context); } add_explicit_key_with_empty_value(old_line, old_indent); diff --git a/single_include/fkYAML/node.hpp b/single_include/fkYAML/node.hpp index d74eb492..cde447ff 100644 --- a/single_include/fkYAML/node.hpp +++ b/single_include/fkYAML/node.hpp @@ -7737,12 +7737,20 @@ class basic_deserializer { last_type == lexical_token_t::END_OF_BUFFER || last_type == lexical_token_t::END_OF_DIRECTIVES || last_type == lexical_token_t::END_OF_DOCUMENT); - // An explicit key at the end of a document has no value either. + // An explicit key at the end of a document has no value either. Its own contents may have left + // more contexts on the stack, so those are unwound first. // ```yaml // ? foo - // # -> {foo: null} + // ? bar: baz + // # -> {foo: null, {bar: baz}: null} // ``` - add_explicit_key_with_null_value(); + while (!m_context_stack.empty()) { + if (m_context_stack.back().state == context_state_t::BLOCK_MAPPING_EXPLICIT_KEY) { + add_explicit_key_with_null_value(); + continue; + } + m_context_stack.pop_back(); + } // reset parameters for the next call. mp_current_node = nullptr; @@ -7955,6 +7963,26 @@ class basic_deserializer { continue; } + { + const parse_context& cur_context = m_context_stack.back(); + const bool is_explicit_key_content = + cur_context.state == context_state_t::BLOCK_MAPPING_EXPLICIT_KEY && cur_context.line == line; + if (is_explicit_key_content) { + // The contents of an explicit key begin with a key separator, so the key is a mapping + // whose first entry has an empty key. Whether that entry has a value, and whether the + // explicit key itself has one, is not known yet. + // ```yaml + // ? : foo + // # ^ this key separator begins the contents of the explicit key + // ``` + *mp_current_node = basic_node_type::mapping(); + apply_directive_set(*mp_current_node); + m_context_stack.emplace_back(line, indent, context_state_t::BLOCK_MAPPING, mp_current_node); + add_empty_key_entry(lexer, token, line, indent); + continue; + } + } + if (m_flow_context_depth > 0) { if (m_context_stack.back().state != context_state_t::MAPPING_VALUE) { // No key precedes this separator, so the entry has an empty key. @@ -8117,8 +8145,25 @@ class basic_deserializer { } // handle explicit mapping key separators. - if FK_YAML_UNLIKELY (m_context_stack.back().state != context_state_t::BLOCK_MAPPING_EXPLICIT_KEY) { - throw parse_error("Unexpected explicit mapping key separator is found.", line, indent); + if (m_context_stack.back().state != context_state_t::BLOCK_MAPPING_EXPLICIT_KEY) { + // The contents of the explicit key may have left their own contexts on the stack. + // ```yaml + // ? : + // : v + // # -> {{null: null}: v} + // ``` + // old_indent is the position of this key separator, while indent already refers to the + // token which follows it. + const auto is_key_context = [old_indent](const parse_context& c) { + return c.state == context_state_t::BLOCK_MAPPING_EXPLICIT_KEY && old_indent == c.indent; + }; + const bool has_key_context = + std::any_of(m_context_stack.rbegin(), m_context_stack.rend(), is_key_context); + if FK_YAML_UNLIKELY (!has_key_context) { + throw parse_error("Unexpected explicit mapping key separator is found.", line, indent); + } + + pop_to_parent_node(old_line, old_indent, is_key_context); } add_explicit_key_with_empty_value(old_line, old_indent); diff --git a/tests/unit_test/test_deserializer_class.cpp b/tests/unit_test/test_deserializer_class.cpp index 3432cc07..18a7b883 100644 --- a/tests/unit_test/test_deserializer_class.cpp +++ b/tests/unit_test/test_deserializer_class.cpp @@ -1797,6 +1797,54 @@ TEST_CASE("Deserializer_ExplicitBlockMapping") { REQUIRE(qux_node["corge"].is_null()); } + SUBCASE("explicit mapping key whose contents begin with a key separator") { + // The contents of the explicit key are a mapping entry with an empty key. + REQUIRE_NOTHROW(root = deserializer.deserialize(fkyaml::detail::input_adapter("? :"))); + + REQUIRE(root.is_mapping()); + REQUIRE(root.size() == 1); + + fkyaml::node key = {{nullptr, nullptr}}; + REQUIRE(root.contains(key)); + REQUIRE(root[key].is_null()); + } + + SUBCASE("explicit mapping key with an empty key and a value") { + REQUIRE_NOTHROW(root = deserializer.deserialize(fkyaml::detail::input_adapter("? : foo"))); + + REQUIRE(root.is_mapping()); + REQUIRE(root.size() == 1); + + fkyaml::node key = {{nullptr, "foo"}}; + REQUIRE(root.contains(key)); + REQUIRE(root[key].is_null()); + } + + SUBCASE("explicit mapping key which is a mapping without a value") { + REQUIRE_NOTHROW(root = deserializer.deserialize(fkyaml::detail::input_adapter("? foo: bar"))); + + REQUIRE(root.is_mapping()); + REQUIRE(root.size() == 1); + + fkyaml::node key = {{"foo", "bar"}}; + REQUIRE(root.contains(key)); + REQUIRE(root[key].is_null()); + } + + SUBCASE("explicit mapping key with an empty key and its own value") { + std::string input = "? :\n" + ": baz\n"; + REQUIRE_NOTHROW(root = deserializer.deserialize(fkyaml::detail::input_adapter(input))); + + REQUIRE(root.is_mapping()); + REQUIRE(root.size() == 1); + + fkyaml::node key = {{nullptr, nullptr}}; + REQUIRE(root.contains(key)); + REQUIRE(root[key].is_string()); + REQUIRE(root[key].as_str() == "baz"); + } + SUBCASE("explicit mapping keys whose values begin on the following lines") { std::string input = "? foo\n" ":\n" From 3cf0d0fb33e246bf49413aab955ad1c5ebeed176 Mon Sep 17 00:00:00 2001 From: Thorek <75499293+sndth@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:09:43 +0200 Subject: [PATCH 3/3] Cover both sides of the omitted value branch for an empty key --- tests/unit_test/test_deserializer_class.cpp | 31 +++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/unit_test/test_deserializer_class.cpp b/tests/unit_test/test_deserializer_class.cpp index 18a7b883..f378d90d 100644 --- a/tests/unit_test/test_deserializer_class.cpp +++ b/tests/unit_test/test_deserializer_class.cpp @@ -79,6 +79,37 @@ TEST_CASE("Deserializer_KeySeparator") { REQUIRE(root[nullptr].as_str() == "empty key"); } + SUBCASE("empty mapping key whose value is omitted") { + std::string input = ":\n" + "bar: baz\n"; + REQUIRE_NOTHROW(root = deserializer.deserialize(fkyaml::detail::input_adapter(input))); + + REQUIRE(root.is_mapping()); + REQUIRE(root.size() == 2); + + REQUIRE(root.contains(nullptr)); + REQUIRE(root[nullptr].is_null()); + + REQUIRE(root.contains("bar")); + REQUIRE(root["bar"].as_str() == "baz"); + } + + SUBCASE("empty mapping key whose value begins on the following line") { + std::string input = ":\n" + " bar: baz\n"; + REQUIRE_NOTHROW(root = deserializer.deserialize(fkyaml::detail::input_adapter(input))); + + REQUIRE(root.is_mapping()); + REQUIRE(root.size() == 1); + REQUIRE(root.contains(nullptr)); + + fkyaml::node& value_node = root[nullptr]; + REQUIRE(value_node.is_mapping()); + REQUIRE(value_node.size() == 1); + REQUIRE(value_node.contains("bar")); + REQUIRE(value_node["bar"].as_str() == "baz"); + } + SUBCASE("empty mapping key in a flow mapping") { REQUIRE_NOTHROW(root = deserializer.deserialize(fkyaml::detail::input_adapter("{key: value, : empty key}"))); REQUIRE(root.is_mapping());