Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions include/xtrpg/xml/node/DeclarationNode.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,20 @@ class DeclarationNode : public INode, public ITagname, public IAttributes {
}

/**
* Explicitly defaulted copy constructor.
* Deleted copy constructor - move-only semantics for consistency and safety.
*/
DeclarationNode(const DeclarationNode &) = default;
DeclarationNode(const DeclarationNode &) = delete;

/**
* Explicitly defaulted move constructor.
*/
DeclarationNode(DeclarationNode &&) = default;

/**
* Explicitly defaulted copy assignment operator.
* Deleted copy assignment operator - move-only semantics for consistency and
* safety.
*/
DeclarationNode &operator=(const DeclarationNode &) = default;
DeclarationNode &operator=(const DeclarationNode &) = delete;

/**
* Explicitly defaulted move assignment operator.
Expand Down
81 changes: 59 additions & 22 deletions include/xtrpg/xml/node/NodeContainer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,36 +18,69 @@ class NodeContainer : public INode {

explicit NodeContainer(NodeType type) : INode(type) {}

~NodeContainer() = default;
// Delete copy semantics - ownership transfer is explicit via move
NodeContainer(const NodeContainer &) = delete;
NodeContainer &operator=(const NodeContainer &) = delete;

// Keep move semantics to allow ownership transfer
NodeContainer(NodeContainer &&) = default;
NodeContainer &operator=(NodeContainer &&) = default;

~NodeContainer() {
for (INode *child : _children) {
delete child;
}
this->_children.clear();
};

/**
* Appends a given child node.
*
* @throws std::invalid_argument if appending would create a cycle.
*/
void append(std::shared_ptr<INode> child) {
if (child) {
// Check if this node is already an ancestor of child by walking up
// the parent chain. This is O(depth) instead of O(n).
for (NodeContainer *ancestor = child->getParent(); ancestor != nullptr;
ancestor = ancestor->getParent()) {
if (ancestor == this) {
throw std::invalid_argument("Cannot create a cycle in XML nodes");
}
void append(INode *ptrChild) {
if (nullptr == ptrChild) {
return;
}

// Check if this node is already an ancestor of child by walking up
// the parent chain. This is O(depth) instead of O(n).
for (NodeContainer *ancestor = ptrChild->getParent(); ancestor != nullptr;
ancestor = ancestor->getParent()) {
if (ancestor == this) {
throw std::invalid_argument("Cannot create a cycle in XML nodes");
}
// Set this node as the child's parent and append
child->setParent(this);
this->_children.push_back(std::move(child));
}
// Set this node as the child's parent and append
ptrChild->setParent(this);
this->_children.push_back(ptrChild);
}

/**
* Returns a vector of child nodes.
* Appends a new TextNode containing the provided string to this container.
* Exception-safe: if append fails, the TextNode is cleaned up before
* re-throwing the exception.
*
* @param withText the text content for the new TextNode
* @throws std::invalid_argument if the text contains invalid XML characters
* or if appending would create a cycle (though cycles are not
* possible with newly created TextNodes).
*/
const std::vector<std::shared_ptr<INode>> &children() const {
return this->_children;
void append(const std::string &withText) {
TextNode *_ptrNode = new TextNode(withText);
try {
this->append(_ptrNode);
} catch (...) {
delete _ptrNode;
throw;
}
}

/**
* Returns a vector of child nodes.
*/
const std::vector<INode *> &children() const { return this->_children; }

/**
* Checks whether this node has any child nodes.
*
Expand All @@ -67,15 +100,14 @@ class NodeContainer : public INode {
}

private:
std::vector<std::shared_ptr<INode>> _children;
std::vector<INode *> _children;
};

/**
* Stream operator overload for easy serialization
*/
inline NodeContainer &operator<<(NodeContainer &node,
std::shared_ptr<INode> child) {
node.append(std::move(child));
inline NodeContainer &operator<<(NodeContainer &node, INode *child) {
node.append(child);
return node;
}

Expand All @@ -85,8 +117,13 @@ inline NodeContainer &operator<<(NodeContainer &node,
*/
inline NodeContainer &operator<<(NodeContainer &node,
const std::string &withText) {
auto textNode = std::make_shared<TextNode>(withText);
node.append(textNode);
TextNode *_ptrNode = new TextNode(withText);
try {
node.append(_ptrNode);
} catch (...) {
delete _ptrNode;
throw;
}
return node;
}
} // namespace xtrpg::xml::node
58 changes: 50 additions & 8 deletions include/xtrpg/xml/node/TagNode.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,21 @@ class TagNode : public ITagname, public IAttributes, public NodeContainer {
: ITagname(name), IAttributes(), NodeContainer(NodeType::TAG) {}

/**
* Explicitly defaulted copy constructor.
* Deleted copy constructor - prevents accidental shallow copies of child
* nodes. Use move semantics for explicit ownership transfer.
*/
TagNode(const TagNode &) = default;
TagNode(const TagNode &) = delete;

/**
* Explicitly defaulted move constructor.
*/
TagNode(TagNode &&) = default;

/**
* Explicitly defaulted copy assignment operator.
* Deleted copy assignment operator - prevents accidental shallow copies of
* child nodes. Use move semantics for explicit ownership transfer.
*/
TagNode &operator=(const TagNode &) = default;
TagNode &operator=(const TagNode &) = delete;

/**
* Explicitly defaulted move assignment operator.
Expand All @@ -55,6 +57,41 @@ class TagNode : public ITagname, public IAttributes, public NodeContainer {
*/
const std::string_view name() const { return this->getTagname(); }

/**
* Appends a new TagNode with the provided tag name to this container.
* The consumer function is called with the new TagNode to allow configuration
* before it is appended. This pattern enables fluent, nested construction of
* XML trees.
*
* Exception-safe: if the consumer throws or append fails, the TagNode is
* cleaned up before re-throwing the exception.
*
* @param tagname the name for the new TagNode
* @param consumer a callable that accepts a TagNode& for configuration
* @throws std::invalid_argument if the tag name is invalid or if the
* consumer throws an exception
*
* Example usage:
* @code
* container.append("error", [](TagNode& error) {
* error.append("message", [](TagNode& msg) {
* msg.append("Something went wrong");
* });
* });
* @endcode
*/
template <typename Consumer>
void append(const std::string &tagname, Consumer &&consumer) {
TagNode *tagNode = new TagNode(tagname);
try {
consumer(*tagNode);
this->NodeContainer::append(tagNode);
} catch (...) {
delete tagNode;
throw;
}
}

/**
* Serializes the node into an XML formatted string.
*/
Expand All @@ -78,8 +115,8 @@ class TagNode : public ITagname, public IAttributes, public NodeContainer {
/**
* Stream operator overload for easy serialization
*/
inline TagNode &operator<<(TagNode &node, std::shared_ptr<INode> child) {
node.append(std::move(child));
inline TagNode &operator<<(TagNode &node, INode *ptrNode) {
node.NodeContainer::append(std::move(ptrNode));
return node;
}

Expand All @@ -88,8 +125,13 @@ inline TagNode &operator<<(TagNode &node, std::shared_ptr<INode> child) {
* child containing the provided text.
*/
inline TagNode &operator<<(TagNode &node, const std::string &withText) {
auto textNode = std::make_shared<TextNode>(withText);
node.append(textNode);
TextNode *_ptrNode = new TextNode(withText);
try {
node.NodeContainer::append(_ptrNode);
} catch (...) {
delete _ptrNode;
throw;
}
return node;
}
} // namespace xtrpg::xml::node
Loading