Skip to content

Feature: Document Creation API #26

@redvers

Description

@redvers

Overview

Add document creation capabilities to the libxml2 Pony binding, enabling users to build XML documents programmatically from scratch. Currently, the library only supports parsing existing XML via parseFile() and parseDoc().

Current State

  • Xml2Doc: Only has parsing constructors (parseFile, parseDoc)
  • Xml2Node: Only wraps existing nodes from parsing/XPath, no creation methods
  • Memory Management: Automatic via _final() destructors and tag references
  • Raw C API: All required libxml2 functions available (xmlNewDoc, xmlNewNode, xmlAddChild, etc.)

Proposed API

Phase 1: Core Document Creation (MVP)

Add fundamental document creation and tree building capabilities.

Xml2Doc Changes (libxml2/xml2doc.pony)

New Constructor:

new create(version: String = "1.0") ? =>
  """
  Create a new empty XML document with the specified version.

  - `version`: XML version string (default: "1.0")

  Creates an empty document with no root element. Use setRootElement()
  or createElement() to build the document tree.

  Example:
    let doc = Xml2Doc.create()?
    let root = doc.createElement("root")?
    doc.setRootElement(root)?
  """
  let ptrx: NullablePointer[XmlDoc] = LibXML2.xmlNewDoc(version)
  if ptrx.is_none() then error end
  ptr' = ptrx

New Factory Method:

fun ref createElement(name: String, content: String = ""): Xml2Node ? =>
  """
  Create a new element node belonging to this document.

  - `name`: Element name (tag name)
  - `content`: Optional text content

  Returns an Xml2Node wrapper. The node is created but not yet attached
  to the document tree. Use setRootElement() or appendChild() to add it.

  Example:
    let doc = Xml2Doc.create()?
    let elem = doc.createElement("item", "Hello")?
    elem.setProp("id", "1")
  """
  let node_ptr = LibXML2.xmlNewDocNode(ptr', NullablePointer[XmlNs].none(),
                                        name, content)
  if node_ptr.is_none() then error end
  Xml2Node.fromPTR(recover tag this end, node_ptr)?

New Method:

fun ref setRootElement(root: Xml2Node): Xml2Node ? =>
  """
  Set the root element of this document.

  - `root`: The node to set as root element

  Returns the old root element if one existed, otherwise returns the new root.
  Raises error if the operation fails.

  Example:
    let doc = Xml2Doc.create()?
    let root = doc.createElement("root")?
    doc.setRootElement(root)?
  """
  let old_root = LibXML2.xmlDocSetRootElement(ptr', root.ptr')
  if old_root.is_none() then
    root  // Return the new root if no previous root existed
  else
    Xml2Node.fromPTR(recover tag this end, old_root)?
  end

Xml2Node Changes (libxml2/xml2node.pony)

New Method:

fun ref appendChild(child: Xml2Node): Xml2Node ? =>
  """
  Add a child node to this element.

  - `child`: Node to add as child

  Returns the added child node. The child is added at the end of the
  children list. Raises error if the operation fails.

  Example:
    let parent = doc.createElement("parent")?
    let child = doc.createElement("child")?
    parent.appendChild(child)?
  """
  let result = LibXML2.xmlAddChild(ptr', child.ptr')
  if result.is_none() then error end
  Xml2Node.fromPTR(xml2doc, result)?

New Method:

fun ref setContent(content: String): None =>
  """
  Set the text content of this node.

  - `content`: Text content to set

  Replaces any existing content of the node. For elements with children,
  this will replace all children with a single text node.

  Example:
    let elem = doc.createElement("item")?
    elem.setContent("New content")
  """
  LibXML2.xmlNodeSetContent(ptr', content)

Testing (libxml2/_tests/basic_tests.pony)

Add comprehensive tests:

  • TestCreateEmptyDocument - Create empty document and serialize
  • TestCreateDocumentWithRoot - Create document, add root, serialize
  • TestCreateAndAppendChildren - Build tree with multiple children
  • TestSetContent - Modify node content after creation
  • TestCreateAndXPath - Verify XPath works on created documents
  • TestCreateAndSaveFile - Create document and save to file

Phase 2: Convenience Methods (Enhancement)

Add helper methods to improve ergonomics.

Xml2Doc Additions

new createWithRoot(root_name: String, version: String = "1.0") ? =>
  """
  Create a new XML document with a root element.

  Convenience constructor that creates a document and sets the root
  element in one step.
  """

fun ref createTextNode(content: String): Xml2Node ? =>
  """
  Create a text node belonging to this document.

  Text nodes are typically added as children of element nodes.
  """

fun ref createComment(content: String): Xml2Node ? =>
  """
  Create a comment node belonging to this document.

  - `content`: Comment text (without <!-- --> delimiters)
  """

Xml2Node Additions

fun ref addChild(name: String, content: String = ""): Xml2Node ? =>
  """
  Convenience method to create and add a child element in one step.

  Creates a new child element, adds it to this node, and returns the
  new child wrapped as Xml2Node.
  """

Testing (libxml2/_tests/coverage_tests.pony)

Add tests:

  • TestCreateWithRootConvenience - Test convenience constructor
  • TestMixedContent - Text nodes + element nodes
  • TestAddChildConvenience - Test addChild helper
  • TestCreateComment - Comment node creation

Implementation Details

Memory Management

  • No changes needed to existing memory management
  • Xml2Doc._final() already calls xmlFreeDoc() which frees entire tree
  • Xml2Node already holds tag reference to Xml2Doc preventing premature freeing
  • All created nodes attached to document via internal xmlDoc* pointer

Error Handling

  • Follow existing pattern: methods that can fail return ?
  • C functions returning NULL pointers trigger Pony errors
  • Pattern: Call C function → check for null → error if null

Compatibility

  • Created documents work with all existing functionality:
    • XPath evaluation (xpathEval, xpathEvalNodes, etc.)
    • Serialization (serialize, saveToFile)
    • Attribute manipulation (setProp, unsetProp, getProp)
    • Tree navigation (getChildren, getRootElement)

C Functions Used

  • xmlNewDoc(version) - Create document
  • xmlNewDocNode(doc, ns, name, content) - Create element
  • xmlDocSetRootElement(doc, root) - Set root
  • xmlAddChild(parent, child) - Add child
  • xmlNodeSetContent(node, content) - Set text
  • xmlNewDocText(doc, content) - Create text node (Phase 2)
  • xmlNewDocComment(doc, content) - Create comment (Phase 2)
  • xmlNewChild(parent, ns, name, content) - Create+add in one call (Phase 2)

Critical Files

To Modify:

  • libxml2/xml2doc.pony (add create constructor and factory methods)
  • libxml2/xml2node.pony (add tree manipulation methods)
  • libxml2/_tests/basic_tests.pony (add Phase 1 tests)
  • libxml2/_tests/coverage_tests.pony (add Phase 2 tests)

To Reference (no changes):

  • libxml2/raw/functions.pony (C API bindings)

Example Usage After Implementation

Phase 1 Example:

// Create a simple document
let doc = Xml2Doc.create()?
let root = doc.createElement("catalog")?
doc.setRootElement(root)?

let book1 = doc.createElement("book")?
book1.setProp("id", "bk101")
let title = doc.createElement("title", "XML Developer's Guide")?
book1.appendChild(title)?
root.appendChild(book1)?

// Serialize and save
let xml_string = doc.serialize()?
doc.saveToFile(auth, "catalog.xml")?

// XPath works on created documents
let books = doc.xpathEvalNodes("//book")?
env.out.print("Found " + books.size().string() + " books")

Phase 2 Example:

// Convenience constructor
let doc = Xml2Doc.createWithRoot("html")?
let root = doc.getRootElement()?

// Convenience addChild method
let body = root.addChild("body")?
let para = body.addChild("p")?

// Mixed content (text + elements)
para.appendChild(doc.createTextNode("This is "))?
let bold = doc.createElement("b", "bold")?
para.appendChild(bold)?
para.appendChild(doc.createTextNode(" text"))?

// Comments
root.appendChild(doc.createComment("Generated by Pony"))?

let html = doc.serialize()?

Verification Plan

After implementing Phase 1:

  1. Run make unit-tests - all existing tests must pass
  2. New tests should create documents, serialize them, and verify output
  3. Test that XPath works on created documents
  4. Test that serialize() and saveToFile() work correctly
  5. Verify memory cleanup with valgrind if available

After implementing Phase 2:

  1. Test convenience constructor works
  2. Test mixed content scenarios (elements + text nodes)
  3. Test addChild convenience method
  4. Test comment creation
  5. Run full test suite with make test

Success Criteria

  • Users can create XML documents from scratch
  • Created documents can be serialized to strings and files
  • XPath queries work on created documents
  • No memory leaks (automatic cleanup via existing destructors)
  • API follows existing Pony idiomatic patterns
  • All tests pass including new creation tests
  • Documentation complete for all new methods

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions