Packages

Lightning-fast XML parsing and XPath querying for Elixir, powered by Rust NIFs

Current section

Files

Jump to
expath lib expath.ex
Raw

lib/expath.ex

defmodule Expath do
@moduledoc """
Lightning-fast XML parsing and XPath querying for Elixir, powered by Rust NIFs.
Expath provides blazing-fast XML processing through Rust's battle-tested `sxd-document`
and `sxd-xpath` libraries, delivering 2-10x performance improvements and up to 195,000x
memory efficiency compared to existing Elixir XML libraries.
## Key Features
- **🚀 Blazing Fast**: 2-10x faster than SweetXml with Rust-powered NIFs
- **🔄 Parse-Once, Query-Many**: Efficient document reuse for multiple XPath queries
- **🛡️ Battle-Tested**: Built on proven Rust XML libraries
- **🎯 Simple API**: Clean, intuitive interface with comprehensive error handling
- **⚡ Thread-Safe**: Safe concurrent access to parsed documents
## Quick Start
For single XPath queries:
iex> xml = "<library><book id='1'><title>1984</title></book></library>"
iex> {:ok, titles} = Expath.select(xml, "//title/text()")
iex> titles
["1984"]
For multiple queries on the same document (more efficient):
iex> xml = "<library><book id='1'><title>1984</title><author>Orwell</author></book></library>"
iex> {:ok, doc} = Expath.new(xml)
iex> {:ok, titles} = Expath.query(doc, "//title/text()")
iex> {:ok, authors} = Expath.query(doc, "//author/text()")
iex> {titles, authors}
{["1984"], ["Orwell"]}
## Performance
Benchmark results comparing Expath vs SweetXml:
| Document Size | Speed |
|---------------|-------|
| Small (644B) | 2-3x faster |
| Medium (5.6KB)| 2.3x faster |
| Large (904KB) | 8-10x faster |
## XPath Support
Expath supports the full XPath 1.0 specification:
# Node selection
Expath.select(xml, "//book") # All book elements
Expath.select(xml, "//book[@id='1']") # Specific book
# Text extraction
Expath.select(xml, "//title/text()") # All title text
Expath.select(xml, "//book/@id") # All id attributes
# Functions
Expath.select(xml, "count(//book)") # Count elements
Expath.select(xml, "//book[position()=1]") # First element
# Complex expressions
Expath.select(xml, "//book[price > 10]/title/text()") # Conditional
## Error Handling
All functions return `{:ok, result}` or `{:error, reason}` tuples:
{:error, :invalid_xml} # XML parsing failed
{:error, :invalid_xpath} # XPath expression invalid
{:error, :xpath_error} # XPath evaluation failed
## When to Use Parse-Once vs Single Query
**Use `select/2` for**:
- One-off XML processing
- Small documents
- Simple scripts
**Use `new/1` + `query/2` for**:
- Multiple queries on the same document
- Large documents (>1KB)
- Performance-critical applications
- Concurrent processing scenarios
"""
# use Rustler, otp_app: :expath, crate: "expath"
version = Mix.Project.config()[:version]
use RustlerPrecompiled,
otp_app: :expath,
crate: "expath",
base_url: "https://github.com/wearecococo/expath/releases/download/v#{version}",
force_build: System.get_env("EXPATH_FORCE_BUILD") in ["1", "true"],
version: version
@doc """
Parse XML from binary data (low-level function).
This function validates that the provided XML binary is well-formed.
For most use cases, prefer `select/2` or `new/1` which provide more functionality.
## Parameters
- `xml_binary` - XML content as binary data
## Returns
- `{:ok, "parsed"}` - XML is valid
- `{:error, :invalid_xml}` - XML parsing failed
## Examples
iex> Expath.parse_xml("<root><item>value</item></root>")
{:ok, "parsed"}
iex> Expath.parse_xml("<invalid><unclosed>")
{:error, :invalid_xml}
"""
def parse_xml(_xml_binary), do: :erlang.nif_error(:nif_not_loaded)
@doc """
Select nodes from XML using XPath expressions (low-level function).
This is a low-level function that parses XML and executes an XPath query in one operation.
For most use cases, prefer the higher-level `select/2` function which provides better
error handling and API consistency.
## Parameters
- `xml_binary` - XML content as binary data
- `xpath_str` - XPath expression as string
## Returns
- `{:ok, results}` - List of matching values as strings
- `{:error, :invalid_xml}` - XML parsing failed
- `{:error, :invalid_xpath}` - XPath expression is invalid
- `{:error, :xpath_error}` - XPath evaluation failed
## Examples
iex> xml = "<root><item>value1</item><item>value2</item></root>"
iex> {:ok, results} = Expath.xpath_select(xml, "//item/text()")
iex> Enum.sort(results)
["value1", "value2"]
iex> Expath.xpath_select("<root/>", "count(//*)")
{:ok, ["1"]}
"""
def xpath_select(_xml_binary, _xpath_str), do: :erlang.nif_error(:nif_not_loaded)
@doc """
Parse XML and return a Document resource for efficient reuse (low-level function).
This is a low-level function that creates an `Expath.Document` resource.
For most use cases, prefer the higher-level `new/1` function.
## Parameters
- `xml_binary` - XML content as binary data
## Returns
- `{:ok, %Expath.Document{}}` - Successfully parsed document
- `{:error, :invalid_xml}` - XML parsing failed
## Examples
iex> {:ok, doc} = Expath.parse_document("<root><item>value</item></root>")
iex> is_struct(doc, Expath.Document)
true
iex> Expath.parse_document("<invalid><unclosed>")
{:error, :invalid_xml}
"""
def parse_document(_xml_binary), do: :erlang.nif_error(:nif_not_loaded)
@doc """
Query a parsed Document resource with XPath (low-level function).
This is a low-level function that queries an `Expath.Document` resource.
For most use cases, prefer the higher-level `query/2` function.
## Parameters
- `document` - An `%Expath.Document{}` resource
- `xpath_str` - XPath expression as string
## Returns
- `{:ok, results}` - List of matching values as strings
- `{:error, :invalid_xml}` - Document is invalid
- `{:error, :invalid_xpath}` - XPath expression is invalid
- `{:error, :xpath_error}` - XPath evaluation failed
## Examples
iex> {:ok, doc} = Expath.parse_document("<root><item>value1</item><item>value2</item></root>")
iex> {:ok, results} = Expath.query_document(doc, "//item/text()")
iex> Enum.sort(results)
["value1", "value2"]
iex> {:ok, doc} = Expath.parse_document("<books><book id='1'/><book id='2'/></books>")
iex> Expath.query_document(doc, "count(//book)")
{:ok, ["2"]}
"""
def query_document(_document, _xpath_str), do: :erlang.nif_error(:nif_not_loaded)
@doc """
Select nodes from an XML string using XPath.
This is the recommended function for single XPath queries. It parses the XML
and executes the XPath query in one operation with clean error handling.
For multiple queries on the same XML document, consider using `new/1` followed
by multiple `query/2` calls for better performance.
## Parameters
- `xml_string` - XML content as a binary string
- `xpath` - XPath expression as a binary string
## Returns
- `{:ok, results}` - List of matching values as strings
- `{:error, :invalid_xml}` - XML parsing failed
- `{:error, :invalid_xpath}` - XPath expression is invalid
- `{:error, :xpath_error}` - XPath evaluation failed
## Examples
iex> xml = "<library><book><title>1984</title></book></library>"
iex> {:ok, titles} = Expath.select(xml, "//title/text()")
iex> titles
["1984"]
iex> xml = "<root><item>value1</item><item>value2</item></root>"
iex> {:ok, results} = Expath.select(xml, "//item/text()")
iex> Enum.sort(results)
["value1", "value2"]
iex> xml = "<books><book id='1'/><book id='2'/></books>"
iex> {:ok, [count]} = Expath.select(xml, "count(//book)")
iex> count
"2"
iex> Expath.select("<invalid><xml>", "//title")
{:error, :invalid_xml}
iex> Expath.select("<root/>", "//[invalid")
{:error, :invalid_xpath}
## Performance Tips
- For single queries: use `select/2`
- For multiple queries on the same document: use `new/1` + `query/2`
- Prefer specific XPath expressions over broad searches
- Use the included benchmark suite to test your specific use case
"""
def select(xml_string, xpath) when is_binary(xml_string) and is_binary(xpath) do
xpath_select(xml_string, xpath)
end
@doc """
Parse XML from a string and return a Document resource.
This is the recommended function for parse-once, query-many scenarios.
The parsed document can be reused for multiple XPath queries without
re-parsing the XML, providing significant performance benefits for
applications that need to run multiple queries on the same document.
The Document resource is automatically cleaned up by Erlang's garbage
collector when it goes out of scope.
## Parameters
- `xml_string` - XML content as a binary string
## Returns
- `{:ok, %Expath.Document{}}` - Successfully parsed document resource
- `{:error, :invalid_xml}` - XML parsing failed
## Examples
iex> xml = "<library><book><title>1984</title><author>Orwell</author></book></library>"
iex> {:ok, doc} = Expath.new(xml)
iex> is_struct(doc, Expath.Document)
true
iex> Expath.new("<invalid><unclosed>")
{:error, :invalid_xml}
## Usage Pattern
# Parse once
{:ok, doc} = Expath.new(large_xml_string)
# Query multiple times efficiently
{:ok, titles} = Expath.query(doc, "//title/text()")
{:ok, authors} = Expath.query(doc, "//author/text()")
{:ok, [count]} = Expath.query(doc, "count(//book)")
# Document automatically cleaned up when `doc` goes out of scope
## Performance Benefits
- **Memory**: Document stored efficiently in Rust
- **Speed**: No XML re-parsing for subsequent queries
- **Concurrency**: Document can be safely shared across processes
- **Large Documents**: Particularly beneficial for documents >1KB
"""
def new(xml_string) when is_binary(xml_string) do
parse_document(xml_string)
end
@doc """
Query a Document resource with XPath.
This function executes XPath queries on a previously parsed Document resource.
It's designed for high-performance scenarios where multiple queries need to
be run on the same XML document without re-parsing.
## Parameters
- `document` - An `%Expath.Document{}` resource from `new/1`
- `xpath` - XPath expression as a binary string
## Returns
- `{:ok, results}` - List of matching values as strings
- `{:error, :invalid_xml}` - Document resource is invalid
- `{:error, :invalid_xpath}` - XPath expression is invalid
- `{:error, :xpath_error}` - XPath evaluation failed
## Examples
iex> xml = "<library><book><title>1984</title><author>Orwell</author></book></library>"
iex> {:ok, doc} = Expath.new(xml)
iex> {:ok, titles} = Expath.query(doc, "//title/text()")
iex> titles
["1984"]
iex> {:ok, doc} = Expath.new("<root><item>value1</item><item>value2</item></root>")
iex> {:ok, results} = Expath.query(doc, "//item/text()")
iex> Enum.sort(results)
["value1", "value2"]
iex> {:ok, doc} = Expath.new("<books><book id='1'/><book id='2'/></books>")
iex> {:ok, [count]} = Expath.query(doc, "count(//book)")
iex> count
"2"
## Typical Usage
# Parse large document once
{:ok, doc} = Expath.new(large_xml_content)
# Run multiple queries efficiently
{:ok, products} = Expath.query(doc, "//product/@id")
{:ok, prices} = Expath.query(doc, "//price/text()")
{:ok, categories} = Expath.query(doc, "//category/text()")
{:ok, [total]} = Expath.query(doc, "count(//product)")
## Concurrent Usage
# Document can be safely shared across processes
{:ok, doc} = Expath.new(xml_content)
tasks = for xpath <- xpath_list do
Task.async(fn -> Expath.query(doc, xpath) end)
end
results = Task.await_many(tasks)
## Performance
This approach is particularly beneficial when:
- Running 2+ queries on the same document
- Working with large documents (>1KB)
- Processing documents in tight loops
- Sharing documents across multiple processes
"""
def query(%Expath.Document{} = document, xpath) when is_binary(xpath) do
query_document(document, xpath)
end
end