Skip to content

Commit

Permalink
Improve some comments, elaborate explanation of NodeRef states
Browse files Browse the repository at this point in the history
  • Loading branch information
biojppm committed Apr 4, 2024
1 parent 25e806b commit c748eef
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 57 deletions.
4 changes: 2 additions & 2 deletions changelog/current.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Fix major error handling problem reported in [#389](https://github.com/biojppm/r
* callback instead of directly raising an exception. */
ConstNodeRef at(csubstr key) const;
/** Likewise, but return a seed node when the key is not found */
ConstNodeRef at(csubstr key);
NodeRef at(csubstr key);

/** Get a child by position, with error checking; complexity is
* O(pos).
Expand All @@ -36,7 +36,7 @@ Fix major error handling problem reported in [#389](https://github.com/biojppm/r
* callback instead of directly raising an exception. */
ConstNodeRef at(size_t pos) const;
/** Likewise, but return a seed node when pos is not found */
ConstNodeRef at(csubstr key);
NodeRef at(csubstr key);
```
- Added macros and respective cmake options to control error handling:
- `RYML_USE_ASSERT` - enable assertions regardless of build type. This is disabled by default.
Expand Down
80 changes: 58 additions & 22 deletions samples/quickstart.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,8 @@ void sample_quick_overview()
// This means that passing a key/index which does not exist will
// not mutate the tree, but will instead store (in the node) the
// proper place of the tree to be able to do so, if and when it is
// required.
// required. This is why the node is said to be in "seed" state -
// it allows creating the entry in the tree in the future.
//
// This is a significant difference from eg, the behavior of
// std::map, which mutates the map immediately within the call to
Expand All @@ -538,50 +539,67 @@ void sample_quick_overview()
// the tree is const, then a NodeRef cannot be obtained from it;
// only a ConstNodeRef, which can never be used to mutate the
// tree.
//
CHECK(!root.has_child("I am not nothing"));
ryml::NodeRef nothing = wroot["I am nothing"];
CHECK(nothing.valid()); // points at the tree, and a specific place in the tree
CHECK(nothing.is_seed()); // ... but nothing is there yet.
CHECK(!root.has_child("I am nothing")); // same as above
CHECK(!nothing.readable()); // ... and this node cannot be used to
// read anything from the tree
ryml::NodeRef something = wroot["I am something"];
ryml::ConstNodeRef constsomething = wroot["I am something"];
CHECK(!root.has_child("I am something")); // same as above
CHECK(something.valid());
CHECK(something.is_seed()); // same as above
CHECK(!constsomething.valid()); // NOTE: because a ConstNodeRef
// cannot be used to mutate a
// tree, it is only valid() if it
// is pointing at an existing
// node.
something = "indeed"; // this will commit to the tree, mutating at the proper place
CHECK(!something.readable()); // same as above
CHECK(!constsomething.valid()); // NOTE: because a ConstNodeRef cannot be
// used to mutate a tree, it is only valid()
// if it is pointing at an existing node.
something = "indeed"; // this will commit the seed to the tree, mutating at the proper place
CHECK(root.has_child("I am something"));
CHECK(root["I am something"].val() == "indeed");
CHECK(something.valid());
CHECK(something.valid()); // it was already valid
CHECK(!something.is_seed()); // now the tree has this node, so the
// ref is no longer a seed
CHECK(something.readable()); // and it is now readable
//
// now the constref is also valid (but it needs to be reassigned):
ryml::ConstNodeRef constsomethingnew = wroot["I am something"];
CHECK(constsomethingnew.valid());
CHECK(constsomethingnew.readable());
// note that the old constref is now stale, because it only keeps
// the state at creation:
CHECK(!constsomething.valid());
CHECK(!constsomething.readable());
//
// -----------------------------------------------------------
// Remember: a seed node cannot be used to read from the tree!
// -----------------------------------------------------------
//
// The seed node needs to be created and become readable first.
//
// Trying to invoke any tree-reading method on a node that is not
// readable will cause an assertion (see RYML_USE_ASSERT).
//
// It is your responsibility to verify that the preconditions are
// met. If you are not sure about the structure of your data,
// write your code defensively to signify your full intent:
//
ryml::NodeRef wbar = wroot["bar"];

Check warning on line 589 in samples/quickstart.cpp

View check run for this annotation

Codecov / codecov/patch

samples/quickstart.cpp#L589

Added line #L589 was not covered by tests
if(wbar.readable() && wbar.is_seq()) // .is_seq() requires .readable()
{
CHECK(wbar[0].readable() && wbar[0].val() == "20");
CHECK( ! wbar[100].readable() || wbar[100].val() == "100"); // <- no crash because it is not .readable(), so never tries to call .val()
// this would work as well:
CHECK( ! wbar[0].is_seed() && wbar[0].val() == "20");
CHECK(wbar[100].is_seed() || wbar[100].val() == "100");
}


//------------------------------------------------------------------
// Emitting:

// emit to a FILE*
ryml::emit_yaml(tree, stdout);
// emit to a stream
std::stringstream ss;
ss << tree;
std::string stream_result = ss.str();
// emit to a buffer:
std::string str_result = ryml::emitrs_yaml<std::string>(tree);
// can emit to any given buffer:
char buf[1024];
ryml::csubstr buf_result = ryml::emit_yaml(tree, buf);
// now check
ryml::csubstr expected_result = R"(foo: says who
bar:
- 20
Expand All @@ -599,12 +617,26 @@ newmap: {}
newmap (serialized): {}
I am something: indeed
)";

// emit to a FILE*
ryml::emit_yaml(tree, stdout);
// emit to a stream
std::stringstream ss;
ss << tree;
std::string stream_result = ss.str();
// emit to a buffer:
std::string str_result = ryml::emitrs_yaml<std::string>(tree);
// can emit to any given buffer:
char buf[1024];
ryml::csubstr buf_result = ryml::emit_yaml(tree, buf);
// now check
CHECK(buf_result == expected_result);
CHECK(str_result == expected_result);
CHECK(stream_result == expected_result);
// There are many possibilities to emit to buffer;
// please look at the emit sample functions below.


//------------------------------------------------------------------
// ConstNodeRef vs NodeRef

Expand Down Expand Up @@ -650,18 +682,22 @@ zh: 行星(气体)
# as per the YAML standard
decode this: "\u263A \xE2\x98\xBA"
and this as well: "\u2705 \U0001D11E"
not decoded: '\u263A \xE2\x98\xBA'
neither this: '\u2705 \U0001D11E'
)");
// in-place UTF8 just works:
CHECK(langs["en"].val() == "Planet (Gas)");
CHECK(langs["fr"].val() == "Planète (Gazeuse)");
CHECK(langs["ru"].val() == "Планета (Газ)");
CHECK(langs["ja"].val() == "惑星(ガス)");
CHECK(langs["zh"].val() == "行星(气体)");
// and \x \u \U codepoints are decoded (but only when they appear
// and \x \u \U codepoints are decoded, but only when they appear
// inside double-quoted strings, as dictated by the YAML
// standard):
// standard:
CHECK(langs["decode this"].val() == "☺ ☺");
CHECK(langs["and this as well"].val() == "✅ 𝄞");
CHECK(langs["not decoded"].val() == "\\u263A \\xE2\\x98\\xBA");
CHECK(langs["neither this"].val() == "\\u2705 \\U0001D11E");

//------------------------------------------------------------------
// Getting the location of nodes in the source:
Expand Down
93 changes: 60 additions & 33 deletions src/c4/yml/node.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -424,8 +424,12 @@ struct RoNodeMethods
/** @name at
*
* These functions are the analogue to operator[], with the
* difference that they */
/** @{ */
* difference that they emit an error instead of an
* assertion. That is, if any of the pre or post conditions is
* violated, an error is always emitted (resulting in a call to
* the error callback).
*
* @{ */

/** Find child by key; complexity is O(num_children).
*
Expand All @@ -440,14 +444,14 @@ struct RoNodeMethods
* to read from the tree.
*
* @warning This method will call the error callback (regardless
* of build type) whenever any of the following preconditions is
* violated: a) the object is valid (points at a tree and a node),
* b) the calling object must be readable (must not be in seed
* state), c) the calling object must be pointing at a MAP
* node. The preconditions are similar to the non-const
* operator[](csubstr), but instead of using assertions, this
* function directly checks those conditions and calls the error
* callback if any of the checks fail.
* of build type or of the value of RYML_USE_ASSERT) whenever any
* of the following preconditions is violated: a) the object is
* valid (points at a tree and a node), b) the calling object must
* be readable (must not be in seed state), c) the calling object
* must be pointing at a MAP node. The preconditions are similar
* to the non-const operator[](csubstr), but instead of using
* assertions, this function directly checks those conditions and
* calls the error callback if any of the checks fail.
*
* @note since it is valid behavior for the returned node to be in
* seed state, the error callback is not invoked when this
Expand Down Expand Up @@ -476,14 +480,14 @@ struct RoNodeMethods
* from the tree.
*
* @warning This method will call the error callback (regardless
* of build type) whenever any of the following preconditions is
* violated: a) the object is valid (points at a tree and a node),
* b) the calling object must be readable (must not be in seed
* state), c) the calling object must be pointing at a MAP
* node. The preconditions are similar to the non-const
* operator[](size_t), but instead of using assertions, this
* function directly checks those conditions and calls the error
* callback if any of the checks fail.
* of build type or of the value of RYML_USE_ASSERT) whenever any
* of the following preconditions is violated: a) the object is
* valid (points at a tree and a node), b) the calling object must
* be readable (must not be in seed state), c) the calling object
* must be pointing at a MAP node. The preconditions are similar
* to the non-const operator[](size_t), but instead of using
* assertions, this function directly checks those conditions and
* calls the error callback if any of the checks fail.
*
* @note since it is valid behavior for the returned node to be in
* seed state, the error callback is not invoked when this
Expand Down Expand Up @@ -751,8 +755,6 @@ struct RoNodeMethods
/** This object holds a pointer to an existing tree, and a node id. It
* can be used only to read from the tree.
*
*
*
* @warning The lifetime of the tree must be larger than that of this
* object. It is up to the user to ensure that this happens. */
class RYML_EXPORT ConstNodeRef : public detail::RoNodeMethods<ConstNodeRef, ConstNodeRef>
Expand Down Expand Up @@ -810,7 +812,8 @@ class RYML_EXPORT ConstNodeRef : public detail::RoNodeMethods<ConstNodeRef, Cons
/** @{ */

C4_ALWAYS_INLINE C4_PURE bool valid() const noexcept { return m_tree != nullptr && m_id != NONE; }
/** because this is a const method, readable() has the same meaning as valid() */
/** because a ConstNodeRef cannot be used to write to the tree,
* readable() has the same meaning as valid() */
C4_ALWAYS_INLINE C4_PURE bool readable() const noexcept { return m_tree != nullptr && m_id != NONE; }
/** because a ConstNodeRef cannot be used to write to the tree, it can never be a seed.
* This method is provided for API equivalence between ConstNodeRef and NodeRef. */
Expand Down Expand Up @@ -852,20 +855,41 @@ class RYML_EXPORT ConstNodeRef : public detail::RoNodeMethods<ConstNodeRef, Cons
//-----------------------------------------------------------------------------

/** A reference to a node in an existing yaml tree, offering a more
* convenient API than the index-based API used in the tree. This
* reference can be used to modify the tree.
* convenient API than the index-based API used in the tree.
*
* Unlike its imutable ConstNodeRef peer, a NodeRef can be used to
* mutate the tree, both by writing to existing nodes and by creating
* new nodes to subsequently write to. So, semantically, a NodeRef
* object can be in one of three states:
*
* ```
* invalid := not pointing at anything
* valid := pointing at a tree/id pair, and further the node can be...
* ` readable := the node exists now
* ` seed := the node may come to exist, if we write to it.
* ```
*
* So both `readable` and `seed` are states where the node is also `valid`.
*
* ```c++
* Tree tree = parse("{a: b}");
* NodeRef invalid; // not pointing at anything.
* NodeRef readable = tree["a"]; // also valid, because "a" exists
* NodeRef seed = tree["none"]; // also valid, but is seed because "none" is not in the map
* ```
*
* When the object is in seed state, using it to read from the tree is
* UB. The seed node can be used to write to the tree provided that
* UB. The seed node can be used to write to the tree, provided that
* its create() method is called prior to writing, which happens in
* most modifying methods in NodeRef. It is the owners's
* responsibility to verify that the an existing node is readable
* before subsequently using it to read from the tree.
* most modifying methods in NodeRef.
*
* See the quickstart for a more detailed explanation on the
* It is the owners's responsibility to verify that an existing
* node is readable before subsequently using it to read from the
* tree.
*
* Individual objects may be
* */
* @warning The lifetime of the tree must be larger than that of this
* object. It is up to the user to ensure that this happens.
*/
class RYML_EXPORT NodeRef : public detail::RoNodeMethods<NodeRef, ConstNodeRef>
{
public:
Expand Down Expand Up @@ -932,11 +956,14 @@ class RYML_EXPORT NodeRef : public detail::RoNodeMethods<NodeRef, ConstNodeRef>

public:

/** @name state queries */
/** @{ */
/** @name state_queries
* @{ */

/** true if the object is pointing at a tree and id. @see the doc for the NodeRef */
inline bool valid() const { return m_tree != nullptr && m_id != NONE; }
inline bool is_seed() const { return m_seed.str != nullptr || m_seed.len != NONE; }
/** true if the object is valid() and in seed state. @see the doc for the NodeRef */
inline bool is_seed() const { return valid() && (m_seed.str != nullptr || m_seed.len != NONE); }
/** true if the object is valid() and NOT in seed state. @see the doc for the NodeRef */
inline bool readable() const { return valid() && !is_seed(); }

inline void _clear_seed() { /*do the following manually or an assert is triggered: */ m_seed.str = nullptr; m_seed.len = NONE; }
Expand Down

0 comments on commit c748eef

Please sign in to comment.