From accc97b4df26fb52f8cf46f47dd9602ddf5e1b42 Mon Sep 17 00:00:00 2001 From: EJainDev Date: Sat, 10 Oct 2026 21:44:11 -0400 Subject: [PATCH] docs: add website based documentation --- .github/workflows/docs.yml | 40 +++ .gitignore | 2 + .readthedocs.yaml | 16 + README.md | 4 + docs/_ext/cpp26.py | 26 ++ docs/_snippets/CMakeLists.txt | 21 ++ docs/_snippets/how-to/attributes.cpp | 15 + docs/_snippets/how-to/combined-schema.cpp | 31 ++ docs/_snippets/how-to/containers.cpp | 30 ++ docs/_snippets/how-to/custom-conversion.cpp | 35 ++ docs/_snippets/how-to/escaping.cpp | 26 ++ docs/_snippets/how-to/external-schema.cpp | 14 + docs/_snippets/how-to/item-names.cpp | 16 + docs/_snippets/how-to/naming.cpp | 15 + docs/_snippets/how-to/nested-objects.cpp | 21 ++ docs/_snippets/how-to/numeric-formatting.cpp | 14 + docs/_snippets/how-to/optional-values.cpp | 17 + docs/_snippets/how-to/read-errors.cpp | 17 + docs/_snippets/how-to/read-objects.cpp | 23 ++ docs/_snippets/how-to/read-setters.cpp | 18 + docs/_snippets/how-to/skip-fields.cpp | 18 + docs/_snippets/how-to/text-and-cdata.cpp | 21 ++ docs/_snippets/how-to/write-documents.cpp | 19 + docs/_snippets/tutorials/catalog-1.cpp | 12 + docs/_snippets/tutorials/catalog-2.cpp | 17 + docs/_snippets/tutorials/catalog.cpp | 22 ++ docs/_snippets/tutorials/product-code-1.cpp | 18 + docs/_snippets/tutorials/product-code.cpp | 36 ++ docs/_snippets/tutorials/settings-1.cpp | 13 + docs/_snippets/tutorials/settings-2.cpp | 15 + docs/_snippets/tutorials/settings.cpp | 23 ++ docs/_static/api-search.js | 56 +++ docs/_static/custom.css | 221 ++++++++++++ docs/_templates/page.html | 20 ++ docs/annotations.md | 73 ++++ docs/api.rst | 344 +++++++++++++++++++ docs/benchmarking.md | 68 ++++ docs/benchmarks.md | 26 ++ docs/building-docs.md | 75 ++++ docs/conf.py | 80 +++++ docs/contributing.md | 103 ++++++ docs/deserialization.md | 134 ++++++++ docs/design.md | 76 ++++ docs/examples.md | 92 +++++ docs/explanation/index.md | 25 ++ docs/explanation/round-trips.md | 73 ++++ docs/how-to/attributes.md | 52 +++ docs/how-to/combined-schema.md | 67 ++++ docs/how-to/containers.md | 55 +++ docs/how-to/custom-conversion.md | 76 ++++ docs/how-to/escaping.md | 54 +++ docs/how-to/external-schema.md | 52 +++ docs/how-to/index.md | 67 ++++ docs/how-to/item-names.md | 60 ++++ docs/how-to/naming.md | 52 +++ docs/how-to/nested-objects.md | 59 ++++ docs/how-to/numeric-formatting.md | 52 +++ docs/how-to/optional-values.md | 54 +++ docs/how-to/simple-record.md | 105 ++++++ docs/how-to/skip-fields.md | 53 +++ docs/how-to/text-and-cdata.md | 54 +++ docs/index.md | 73 ++++ docs/installation.md | 109 ++++++ docs/license.md | 6 + docs/project/index.md | 12 + docs/quickstart.md | 118 +++++++ docs/reference/index.md | 32 ++ docs/requirements.txt | 34 ++ docs/serialization.md | 97 ++++++ docs/tutorials/catalog.md | 103 ++++++ docs/tutorials/index.md | 45 +++ docs/tutorials/product-code.md | 89 +++++ docs/tutorials/schema.md | 118 +++++++ docs/tutorials/settings.md | 99 ++++++ docs/xml-input.md | 205 +++++++++++ 75 files changed, 4133 insertions(+) create mode 100644 .github/workflows/docs.yml create mode 100644 .readthedocs.yaml create mode 100644 docs/_ext/cpp26.py create mode 100644 docs/_snippets/CMakeLists.txt create mode 100644 docs/_snippets/how-to/attributes.cpp create mode 100644 docs/_snippets/how-to/combined-schema.cpp create mode 100644 docs/_snippets/how-to/containers.cpp create mode 100644 docs/_snippets/how-to/custom-conversion.cpp create mode 100644 docs/_snippets/how-to/escaping.cpp create mode 100644 docs/_snippets/how-to/external-schema.cpp create mode 100644 docs/_snippets/how-to/item-names.cpp create mode 100644 docs/_snippets/how-to/naming.cpp create mode 100644 docs/_snippets/how-to/nested-objects.cpp create mode 100644 docs/_snippets/how-to/numeric-formatting.cpp create mode 100644 docs/_snippets/how-to/optional-values.cpp create mode 100644 docs/_snippets/how-to/read-errors.cpp create mode 100644 docs/_snippets/how-to/read-objects.cpp create mode 100644 docs/_snippets/how-to/read-setters.cpp create mode 100644 docs/_snippets/how-to/skip-fields.cpp create mode 100644 docs/_snippets/how-to/text-and-cdata.cpp create mode 100644 docs/_snippets/how-to/write-documents.cpp create mode 100644 docs/_snippets/tutorials/catalog-1.cpp create mode 100644 docs/_snippets/tutorials/catalog-2.cpp create mode 100644 docs/_snippets/tutorials/catalog.cpp create mode 100644 docs/_snippets/tutorials/product-code-1.cpp create mode 100644 docs/_snippets/tutorials/product-code.cpp create mode 100644 docs/_snippets/tutorials/settings-1.cpp create mode 100644 docs/_snippets/tutorials/settings-2.cpp create mode 100644 docs/_snippets/tutorials/settings.cpp create mode 100644 docs/_static/api-search.js create mode 100644 docs/_static/custom.css create mode 100644 docs/_templates/page.html create mode 100644 docs/annotations.md create mode 100644 docs/api.rst create mode 100644 docs/benchmarking.md create mode 100644 docs/benchmarks.md create mode 100644 docs/building-docs.md create mode 100644 docs/conf.py create mode 100644 docs/contributing.md create mode 100644 docs/deserialization.md create mode 100644 docs/design.md create mode 100644 docs/examples.md create mode 100644 docs/explanation/index.md create mode 100644 docs/explanation/round-trips.md create mode 100644 docs/how-to/attributes.md create mode 100644 docs/how-to/combined-schema.md create mode 100644 docs/how-to/containers.md create mode 100644 docs/how-to/custom-conversion.md create mode 100644 docs/how-to/escaping.md create mode 100644 docs/how-to/external-schema.md create mode 100644 docs/how-to/index.md create mode 100644 docs/how-to/item-names.md create mode 100644 docs/how-to/naming.md create mode 100644 docs/how-to/nested-objects.md create mode 100644 docs/how-to/numeric-formatting.md create mode 100644 docs/how-to/optional-values.md create mode 100644 docs/how-to/simple-record.md create mode 100644 docs/how-to/skip-fields.md create mode 100644 docs/how-to/text-and-cdata.md create mode 100644 docs/index.md create mode 100644 docs/installation.md create mode 100644 docs/license.md create mode 100644 docs/project/index.md create mode 100644 docs/quickstart.md create mode 100644 docs/reference/index.md create mode 100644 docs/requirements.txt create mode 100644 docs/serialization.md create mode 100644 docs/tutorials/catalog.md create mode 100644 docs/tutorials/index.md create mode 100644 docs/tutorials/product-code.md create mode 100644 docs/tutorials/schema.md create mode 100644 docs/tutorials/settings.md create mode 100644 docs/xml-input.md diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..c1b878b --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,40 @@ +name: Documentation + +on: + push: + branches: [main] + paths: + - 'docs/**' + - '.readthedocs.yaml' + - '.github/workflows/docs.yml' + - 'include/**' + - 'examples/**' + - 'assets/**' + - 'LICENSE' + - 'README.md' + pull_request: + paths: + - 'docs/**' + - '.readthedocs.yaml' + - '.github/workflows/docs.yml' + - 'include/**' + - 'examples/**' + - 'assets/**' + - 'LICENSE' + - 'README.md' + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + cache: pip + cache-dependency-path: docs/requirements.txt + - run: python -m pip install -r docs/requirements.txt + - run: python -m sphinx -b html -n -W --keep-going docs build/docs/html diff --git a/.gitignore b/.gitignore index aa5c13d..71c9f87 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,11 @@ .* build/ +__pycache__/ !.github !.devcontainer +!.readthedocs.yaml !.gitignore !.gitattributes !.clang-format diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..81b06fd --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,16 @@ +# Read the Docs configuration file +# https://docs.readthedocs.io/en/stable/config-file/v2.html +version: 2 + +build: + os: ubuntu-24.04 + tools: + python: "3.13" + +sphinx: + configuration: docs/conf.py + fail_on_warning: true + +python: + install: + - requirements: docs/requirements.txt diff --git a/README.md b/README.md index 8401f9f..04d6922 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,10 @@ XML, missing required members, and invalid values through **Requires GCC 16.1+, CMake 4.3.3+, and Ninja.** The dev container provides a matching environment. +The full documentation website lives in [`docs/`](docs/), with guides, examples, +and an indexed API reference. See [Building the documentation](docs/building-docs.md) +for local previews and Read the Docs setup. + ## Table of Contents - [Benchmarks](#benchmarks) diff --git a/docs/_ext/cpp26.py b/docs/_ext/cpp26.py new file mode 100644 index 0000000..1007130 --- /dev/null +++ b/docs/_ext/cpp26.py @@ -0,0 +1,26 @@ +"""Highlight C++26 annotation expressions, including their whitespace and braces.""" + +from pygments.lexer import bygroups, include, inherit +from pygments.lexers import CppLexer +from pygments.token import Operator, Punctuation, Whitespace + + +class Cpp26Lexer(CppLexer): + tokens = { + state: [ + (r"(\[\[)(\s*)(=)", bygroups(Punctuation, Whitespace, Operator), "annotation-expression"), + inherit, + ] + for state in ("statements", "classname", "enumname") + } + tokens["annotation-expression"] = [ + (r"\]\]", Punctuation, "#pop"), + (r"[{}]", Punctuation), + include("whitespace"), + include("statements"), + ] + + +def setup(app): + app.add_lexer("cpp", Cpp26Lexer) + return {"version": "1.0", "parallel_read_safe": True, "parallel_write_safe": True} diff --git a/docs/_snippets/CMakeLists.txt b/docs/_snippets/CMakeLists.txt new file mode 100644 index 0000000..b45c49c --- /dev/null +++ b/docs/_snippets/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 4.3.3) +# import std is experimental; its opt-in key depends on the CMake version. +if(CMAKE_VERSION VERSION_LESS 4.4) + set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD "451f2fe2-a8a2-47c3-bc32-94786d8fc91b") +else() + set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD "f35a9ac6-8463-4d38-8eec-5d6008153e7d") +endif() +set(CMAKE_CXX_STANDARD 26) +set(CMAKE_CXX_MODULE_STD ON) +project(my_app LANGUAGES CXX) + +include(FetchContent) +FetchContent_Declare( + serial_xml + GIT_REPOSITORY https://github.com/EJainDev/SerialXML.git + GIT_TAG main +) +FetchContent_MakeAvailable(serial_xml) + +add_executable(my_app main.cpp) +target_link_libraries(my_app PRIVATE serial_xml::serial_xml) diff --git a/docs/_snippets/how-to/attributes.cpp b/docs/_snippets/how-to/attributes.cpp new file mode 100644 index 0000000..8ec51f2 --- /dev/null +++ b/docs/_snippets/how-to/attributes.cpp @@ -0,0 +1,15 @@ +import std; +import serial_xml; + +struct AttributeAndChild { + [[= serial_xml::attribute]] int x; + int y; +}; + +int main() { + const AttributeAndChild original{4, 5}; + const auto xml = serial_xml::to_xml(original, false); + std::println("{}", xml); + const auto restored = serial_xml::from_xml(xml); + std::println("x: {}; y: {}", restored.x, restored.y); +} diff --git a/docs/_snippets/how-to/combined-schema.cpp b/docs/_snippets/how-to/combined-schema.cpp new file mode 100644 index 0000000..657c184 --- /dev/null +++ b/docs/_snippets/how-to/combined-schema.cpp @@ -0,0 +1,31 @@ +import std; +import serial_xml; + +struct Address { + std::string street; + [[= serial_xml::name{"city_name"}]] std::string city; + int zip_code; +}; + +struct Employee { + [[ = serial_xml::name{"emp_id"}, = serial_xml::attribute ]] int id; + std::string name; + Address address; + [[= serial_xml::iter{"skill", "skills"}]] std::vector skills; +}; + +struct Department { + [[ = serial_xml::name{"dept_name"}, = serial_xml::attribute ]] std::string name; + [[= serial_xml::name{"team"}]] std::vector employees; +}; + +int main() { + const Employee employee{1, "Alice", {"123 Main St", "Boston", 2128}, {"C++", "Python"}}; + const Department original{"Engineering", {employee}}; + const auto xml = serial_xml::to_xml(original, false); + std::println("{}", serial_xml::prettify(xml)); + const auto restored = serial_xml::from_xml(xml); + const auto& first = restored.employees.front(); + std::println("Department: {}; employee: {}; city: {}; skills: {}", restored.name, first.name, + first.address.city, first.skills.size()); +} diff --git a/docs/_snippets/how-to/containers.cpp b/docs/_snippets/how-to/containers.cpp new file mode 100644 index 0000000..5c8cd57 --- /dev/null +++ b/docs/_snippets/how-to/containers.cpp @@ -0,0 +1,30 @@ +import std; +import serial_xml; + +struct VectorExample { + std::vector values; +}; + +struct ExcludeOnEmpty { + [[= serial_xml::exclude_on_empty]] std::vector values; +}; + +struct NoIter { + [[= serial_xml::no_iter]] std::vector values; +}; + +int main() { + const auto iterated = serial_xml::to_xml(VectorExample{{1, 2, 3}}, false); + const auto empty = serial_xml::to_xml(VectorExample{{}}, false); + const auto omitted = serial_xml::to_xml(ExcludeOnEmpty{{}}, false); + const auto text = serial_xml::to_xml(NoIter{{1, 2, 3}}, false); + std::println("{}", iterated); + std::println("{}", empty); + std::println("{}", omitted); + std::println("{}", text); + const auto restored = serial_xml::from_xml(iterated); + const auto restored_text = serial_xml::from_xml(text); + const auto restored_empty = serial_xml::from_xml(omitted); + std::println("Items: {}; text items: {}; omitted items: {}", restored.values.size(), + restored_text.values.size(), restored_empty.values.size()); +} diff --git a/docs/_snippets/how-to/custom-conversion.cpp b/docs/_snippets/how-to/custom-conversion.cpp new file mode 100644 index 0000000..1836186 --- /dev/null +++ b/docs/_snippets/how-to/custom-conversion.cpp @@ -0,0 +1,35 @@ +import std; +import serial_xml; + +struct ProductCode { + int value; +}; + +std::string format_code(const ProductCode& code) { return "SKU-" + std::to_string(code.value); } + +template <> +ProductCode serial_xml::from_string(std::string_view text) { + if (!text.starts_with("SKU-")) { + throw serial_xml::deserialization_error("Expected SKU- code"); + } + return {serial_xml::from_string(text.substr(4))}; +} + +struct Product { + [[= serial_xml::attribute]] int id; + [[= serial_xml::format{format_code}]] ProductCode code; +}; + +int main() { + const Product original{7, {42}}; + const auto xml = serial_xml::to_xml(original, false); + std::println("{}", xml); + const auto restored = serial_xml::from_xml(xml); + std::println("Product: {}; code: {}", restored.id, restored.code.value); + try { + const auto invalid = serial_xml::from_string("OTHER-42"); + std::println("{}", invalid.value); + } catch (const serial_xml::deserialization_error& error) { + std::println("Conversion failed: {}", error.what()); + } +} diff --git a/docs/_snippets/how-to/escaping.cpp b/docs/_snippets/how-to/escaping.cpp new file mode 100644 index 0000000..2af45fd --- /dev/null +++ b/docs/_snippets/how-to/escaping.cpp @@ -0,0 +1,26 @@ +import std; +import serial_xml; + +struct EscapeChild { + std::string text; +}; + +struct EscapeAttribute { + [[= serial_xml::attribute]] std::string text; +}; + +struct EscapeRaw { + [[= serial_xml::raw]] std::string text; +}; + +int main() { + const std::string special = "<>&'\""; + const auto child = serial_xml::to_xml(EscapeChild{special}, false); + const auto attribute = serial_xml::to_xml(EscapeAttribute{special}, false); + const auto raw = serial_xml::to_xml(EscapeRaw{special}, false); + std::println("{}", child); + std::println("{}", attribute); + std::println("{}", raw); + const auto restored = serial_xml::from_xml(child); + std::println("Restored text: {}", restored.text); +} diff --git a/docs/_snippets/how-to/external-schema.cpp b/docs/_snippets/how-to/external-schema.cpp new file mode 100644 index 0000000..666ae83 --- /dev/null +++ b/docs/_snippets/how-to/external-schema.cpp @@ -0,0 +1,14 @@ +import std; +import serial_xml; + +class mock_vector { + public: + [[= serial_xml::attribute]] std::size_t size() const; +}; + +int main() { + const std::vector values{1, 2, 3}; + const auto xml = serial_xml::to_xml(values, false); + std::println("{}", xml); + std::println("Actual vector size: {}", values.size()); +} diff --git a/docs/_snippets/how-to/item-names.cpp b/docs/_snippets/how-to/item-names.cpp new file mode 100644 index 0000000..ef77d25 --- /dev/null +++ b/docs/_snippets/how-to/item-names.cpp @@ -0,0 +1,16 @@ +import std; +import serial_xml; + +struct Numbers { + [[= serial_xml::iter{"number", "numbers"}]] std::vector values; +}; + +int main() { + const Numbers original{{1, 2, 3}}; + const auto xml = serial_xml::to_xml(original, false); + std::println("{}", serial_xml::prettify(xml)); + const auto restored = serial_xml::from_xml(xml); + for (const int value : restored.values) { + std::println("Number: {}", value); + } +} diff --git a/docs/_snippets/how-to/naming.cpp b/docs/_snippets/how-to/naming.cpp new file mode 100644 index 0000000..3c78289 --- /dev/null +++ b/docs/_snippets/how-to/naming.cpp @@ -0,0 +1,15 @@ +import std; +import serial_xml; + +struct[[= serial_xml::name{"person"}]] Person { + [[= serial_xml::attribute]] int age; + [[= serial_xml::name{"food"}]] std::string favorite_food; +}; + +int main() { + const Person original{3, "pizza"}; + const auto xml = serial_xml::to_xml(original, false); + std::println("{}", xml); + const auto restored = serial_xml::from_xml(xml); + std::println("Age: {}; food: {}", restored.age, restored.favorite_food); +} diff --git a/docs/_snippets/how-to/nested-objects.cpp b/docs/_snippets/how-to/nested-objects.cpp new file mode 100644 index 0000000..4af59ef --- /dev/null +++ b/docs/_snippets/how-to/nested-objects.cpp @@ -0,0 +1,21 @@ +import std; +import serial_xml; + +struct Address { + std::string street; + std::string city; + int zip; +}; + +struct Person { + std::string name; + Address address; +}; + +int main() { + const Person original{"Alice", {"123 Main St", "Boston", 2128}}; + const auto xml = serial_xml::to_xml(original, false); + std::println("{}", serial_xml::prettify(xml)); + const auto restored = serial_xml::from_xml(xml); + std::println("{} lives in {} ({})", restored.name, restored.address.city, restored.address.zip); +} diff --git a/docs/_snippets/how-to/numeric-formatting.cpp b/docs/_snippets/how-to/numeric-formatting.cpp new file mode 100644 index 0000000..15332e8 --- /dev/null +++ b/docs/_snippets/how-to/numeric-formatting.cpp @@ -0,0 +1,14 @@ +import std; +import serial_xml; + +struct FormattedAttribute { + [[ = serial_xml::attribute, = serial_xml::format{"03d"} ]] int x; +}; + +int main() { + const FormattedAttribute original{42}; + const auto xml = serial_xml::to_xml(original, false); + std::println("{}", xml); + const auto restored = serial_xml::from_xml(xml); + std::println("Restored value: {}", restored.x); +} diff --git a/docs/_snippets/how-to/optional-values.cpp b/docs/_snippets/how-to/optional-values.cpp new file mode 100644 index 0000000..2e03583 --- /dev/null +++ b/docs/_snippets/how-to/optional-values.cpp @@ -0,0 +1,17 @@ +import std; +import serial_xml; + +struct OptionalChild { + std::optional value; +}; + +int main() { + const auto present = serial_xml::to_xml(OptionalChild{{42}}, false); + const auto absent = serial_xml::to_xml(OptionalChild{std::nullopt}, false); + std::println("{}", present); + std::println("{}", absent); + auto restored = serial_xml::from_xml(present); + std::println("Present value: {}", restored.value.value()); + serial_xml::from_xml(restored, absent); + std::println("After absent input: {}", restored.value.has_value()); +} diff --git a/docs/_snippets/how-to/read-errors.cpp b/docs/_snippets/how-to/read-errors.cpp new file mode 100644 index 0000000..f8fd3f8 --- /dev/null +++ b/docs/_snippets/how-to/read-errors.cpp @@ -0,0 +1,17 @@ +import std; +import serial_xml; + +struct Record { + int value; +}; + +int main() { + const std::string xml = "not-a-number"; + try { + const auto record = serial_xml::from_xml(xml); + std::println("Value: {}", record.value); + } catch (const serial_xml::deserialization_error& error) { + std::println("Read failed"); + std::println("{}", error.what()); + } +} diff --git a/docs/_snippets/how-to/read-objects.cpp b/docs/_snippets/how-to/read-objects.cpp new file mode 100644 index 0000000..add3044 --- /dev/null +++ b/docs/_snippets/how-to/read-objects.cpp @@ -0,0 +1,23 @@ +import std; +import serial_xml; + +struct Person { + [[= serial_xml::attribute]] int age; + std::string name; + [[= serial_xml::optional]] std::string nickname = "unknown"; +}; + +int main() { + const std::string initial_xml = "Alex"; + auto person = serial_xml::from_xml(initial_xml); + std::println("{}: age {}; nickname {}", person.name, person.age, person.nickname); + + person.nickname = "Ace"; + const std::string replacement_xml = "Sam"; + serial_xml::from_xml(person, replacement_xml); + std::println("{}: age {}; nickname {}", person.name, person.age, person.nickname); + + auto replacement = serial_xml::from_xml(initial_xml); + person = std::move(replacement); + std::println("Replaced: {}; nickname {}", person.name, person.nickname); +} diff --git a/docs/_snippets/how-to/read-setters.cpp b/docs/_snippets/how-to/read-setters.cpp new file mode 100644 index 0000000..9c803cc --- /dev/null +++ b/docs/_snippets/how-to/read-setters.cpp @@ -0,0 +1,18 @@ +import std; +import serial_xml; + +class Account { + public: + [[= serial_xml::skip]] int balance() const { return balance_; } + [[ = serial_xml::setter, = serial_xml::name{"balance"} ]] void deposit_balance(int amount) { + balance_ = amount; + } + + private: + int balance_ = 0; +}; + +int main() { + const auto account = serial_xml::from_xml("125"); + std::println("Balance: {}", account.balance()); +} diff --git a/docs/_snippets/how-to/skip-fields.cpp b/docs/_snippets/how-to/skip-fields.cpp new file mode 100644 index 0000000..a86aef7 --- /dev/null +++ b/docs/_snippets/how-to/skip-fields.cpp @@ -0,0 +1,18 @@ +import std; +import serial_xml; + +struct Record { + int value; + [[= serial_xml::skip]] int cached_value = -1; +}; + +int main() { + const Record original{42, 100}; + const auto xml = serial_xml::to_xml(original, false); + std::println("{}", xml); + auto restored = serial_xml::from_xml(xml); + std::println("New object: value {}; cache {}", restored.value, restored.cached_value); + restored.cached_value = 250; + serial_xml::from_xml(restored, xml); + std::println("Updated object: cache {}", restored.cached_value); +} diff --git a/docs/_snippets/how-to/text-and-cdata.cpp b/docs/_snippets/how-to/text-and-cdata.cpp new file mode 100644 index 0000000..1cc13ef --- /dev/null +++ b/docs/_snippets/how-to/text-and-cdata.cpp @@ -0,0 +1,21 @@ +import std; +import serial_xml; + +struct RawExample { + [[= serial_xml::raw]] std::string text; +}; + +struct CDataExample { + [[= serial_xml::cdata]] std::string content; +}; + +int main() { + const auto raw_xml = serial_xml::to_xml(RawExample{"Hello & friends"}, false); + const auto cdata_xml = serial_xml::to_xml(CDataExample{"text & stuff"}, false); + std::println("{}", raw_xml); + std::println("{}", cdata_xml); + const auto raw_value = serial_xml::from_xml(raw_xml); + const auto cdata_value = serial_xml::from_xml(cdata_xml); + std::println("Raw value: {}", raw_value.text); + std::println("CDATA value: {}", cdata_value.content); +} diff --git a/docs/_snippets/how-to/write-documents.cpp b/docs/_snippets/how-to/write-documents.cpp new file mode 100644 index 0000000..4002464 --- /dev/null +++ b/docs/_snippets/how-to/write-documents.cpp @@ -0,0 +1,19 @@ +import std; +import serial_xml; + +struct[[= serial_xml::name{"record"}]] Record { + int value; +}; + +int main() { + const Record record{42}; + const auto document = serial_xml::to_xml(record); + const auto fragment = serial_xml::to_xml(record, false); + const auto named = serial_xml::to_xml(record, false, "entry"); + std::println("Document:\n{}", document); + std::println("Fragment:\n{}", fragment); + std::println("Named root:\n{}", named); + std::println("For display:\n{}", serial_xml::prettify(named)); + const auto restored = serial_xml::from_xml(named, "entry"); + std::println("Restored value: {}", restored.value); +} diff --git a/docs/_snippets/tutorials/catalog-1.cpp b/docs/_snippets/tutorials/catalog-1.cpp new file mode 100644 index 0000000..e681d93 --- /dev/null +++ b/docs/_snippets/tutorials/catalog-1.cpp @@ -0,0 +1,12 @@ +import std; +import serial_xml; + +struct Book { + [[= serial_xml::attribute]] int id; + std::string title; +}; + +int main() { + const Book book{1, "The XML Handbook"}; + std::println("{}", serial_xml::to_xml(book, false)); +} diff --git a/docs/_snippets/tutorials/catalog-2.cpp b/docs/_snippets/tutorials/catalog-2.cpp new file mode 100644 index 0000000..ebddf32 --- /dev/null +++ b/docs/_snippets/tutorials/catalog-2.cpp @@ -0,0 +1,17 @@ +import std; +import serial_xml; + +struct Book { + [[= serial_xml::attribute]] int id; + std::string title; +}; + +struct[[= serial_xml::name{"catalog"}]] Catalog { + [[= serial_xml::iter{"book"}]] std::vector books; +}; + +int main() { + const Catalog original{{{1, "The XML Handbook"}, {2, "Reflection in C++"}}}; + const auto xml = serial_xml::to_xml(original, false); + std::println("{}", serial_xml::prettify(xml)); +} diff --git a/docs/_snippets/tutorials/catalog.cpp b/docs/_snippets/tutorials/catalog.cpp new file mode 100644 index 0000000..d186d39 --- /dev/null +++ b/docs/_snippets/tutorials/catalog.cpp @@ -0,0 +1,22 @@ +import std; +import serial_xml; + +struct Book { + [[= serial_xml::attribute]] int id; + std::string title; +}; + +struct[[= serial_xml::name{"catalog"}]] Catalog { + [[= serial_xml::iter{"book"}]] std::vector books; +}; + +int main() { + const Catalog original{{{1, "The XML Handbook"}, {2, "Reflection in C++"}}}; + const auto xml = serial_xml::to_xml(original, false); + std::println("{}", serial_xml::prettify(xml)); + const auto restored = serial_xml::from_xml(xml); + std::println("Restored books: {}", restored.books.size()); + for (const auto& book : restored.books) { + std::println("{}: {}", book.id, book.title); + } +} diff --git a/docs/_snippets/tutorials/product-code-1.cpp b/docs/_snippets/tutorials/product-code-1.cpp new file mode 100644 index 0000000..f3ded4f --- /dev/null +++ b/docs/_snippets/tutorials/product-code-1.cpp @@ -0,0 +1,18 @@ +import std; +import serial_xml; + +struct ProductCode { + int value; +}; + +std::string format_code(const ProductCode& code) { return "SKU-" + std::to_string(code.value); } + +struct Product { + [[= serial_xml::attribute]] int id; + [[= serial_xml::format{format_code}]] ProductCode code; +}; + +int main() { + const Product original{7, {42}}; + std::println("{}", serial_xml::to_xml(original, false)); +} diff --git a/docs/_snippets/tutorials/product-code.cpp b/docs/_snippets/tutorials/product-code.cpp new file mode 100644 index 0000000..39da312 --- /dev/null +++ b/docs/_snippets/tutorials/product-code.cpp @@ -0,0 +1,36 @@ +import std; +import serial_xml; + +struct ProductCode { + int value; +}; + +std::string format_code(const ProductCode& code) { return "SKU-" + std::to_string(code.value); } + +template <> +ProductCode serial_xml::from_string(std::string_view text) { + if (!text.starts_with("SKU-")) { + throw serial_xml::deserialization_error("Expected SKU- code"); + } + return {serial_xml::from_string(text.substr(4))}; +} + +struct Product { + [[= serial_xml::attribute]] int id; + [[= serial_xml::format{format_code}]] ProductCode code; +}; + +int main() { + const Product original{7, {42}}; + const auto xml = serial_xml::to_xml(original, false); + std::println("{}", xml); + const auto restored = serial_xml::from_xml(xml); + std::println("Restored product: {}; code: {}", restored.id, restored.code.value); + try { + const auto invalid = + serial_xml::from_xml("OTHER-42"); + std::println("Code: {}", invalid.code.value); + } catch (const serial_xml::deserialization_error& error) { + std::println("Rejected input: {}", error.what()); + } +} diff --git a/docs/_snippets/tutorials/settings-1.cpp b/docs/_snippets/tutorials/settings-1.cpp new file mode 100644 index 0000000..4b01e37 --- /dev/null +++ b/docs/_snippets/tutorials/settings-1.cpp @@ -0,0 +1,13 @@ +import std; +import serial_xml; + +struct[[= serial_xml::name{"settings"}]] Preferences { + int volume; + std::string theme = "light"; +}; + +int main() { + const auto preferences = serial_xml::from_xml( + "20dark"); + std::println("Volume: {}; theme: {}", preferences.volume, preferences.theme); +} diff --git a/docs/_snippets/tutorials/settings-2.cpp b/docs/_snippets/tutorials/settings-2.cpp new file mode 100644 index 0000000..fdac791 --- /dev/null +++ b/docs/_snippets/tutorials/settings-2.cpp @@ -0,0 +1,15 @@ +import std; +import serial_xml; + +struct[[= serial_xml::name{"settings"}]] Preferences { + int volume; + [[= serial_xml::optional]] std::string theme = "light"; +}; + +int main() { + auto preferences = serial_xml::from_xml("20"); + std::println("Initial: volume {}; theme {}", preferences.volume, preferences.theme); + preferences.theme = "dark"; + serial_xml::from_xml(preferences, "35"); + std::println("Updated: volume {}; theme {}", preferences.volume, preferences.theme); +} diff --git a/docs/_snippets/tutorials/settings.cpp b/docs/_snippets/tutorials/settings.cpp new file mode 100644 index 0000000..9f2dc99 --- /dev/null +++ b/docs/_snippets/tutorials/settings.cpp @@ -0,0 +1,23 @@ +import std; +import serial_xml; + +struct[[= serial_xml::name{"settings"}]] Preferences { + int volume; + [[= serial_xml::optional]] std::string theme = "light"; +}; + +int main() { + auto preferences = serial_xml::from_xml("20"); + std::println("Initial: volume {}; theme {}", preferences.volume, preferences.theme); + preferences.theme = "dark"; + serial_xml::from_xml(preferences, "35"); + std::println("Updated: volume {}; theme {}", preferences.volume, preferences.theme); + try { + const auto invalid = + serial_xml::from_xml("loud"); + std::println("Volume: {}", invalid.volume); + } catch (const serial_xml::deserialization_error& error) { + std::println("Rejected input: {}", error.what()); + } + std::println("{}", serial_xml::to_xml(preferences, false)); +} diff --git a/docs/_static/api-search.js b/docs/_static/api-search.js new file mode 100644 index 0000000..0f1b2f0 --- /dev/null +++ b/docs/_static/api-search.js @@ -0,0 +1,56 @@ +/* Build a local symbol index from Sphinx's rendered declarations. */ +document.addEventListener("DOMContentLoaded", () => { + const form = document.querySelector(".api-search"); + if (!form) return; + + const input = form.querySelector("input"); + const results = document.getElementById("api-search-results"); + const status = document.getElementById("api-search-status"); + const declarations = Array.from(document.querySelectorAll("article dl.cpp > dt.sig[id]"), (signature) => { + const label = signature.cloneNode(true); + label.querySelectorAll(".headerlink").forEach((link) => link.remove()); + label.querySelectorAll("br").forEach((br) => br.replaceWith(" ")); + return { id: signature.id, label: label.textContent.trim().replace(/\s+/g, " ") }; + }); + + function search() { + const query = input.value.trim().toLowerCase(); + results.replaceChildren(); + results.hidden = !query; + if (!query) { + status.textContent = "Type to find matching API declarations."; + return; + } + const matches = declarations.filter((symbol) => symbol.label.toLowerCase().includes(query)); + status.textContent = matches.length + ? `${matches.length} matching declaration${matches.length === 1 ? "" : "s"}.` + : "No matching API declarations. Try a different symbol name."; + for (const symbol of matches) { + const item = document.createElement("li"); + const link = document.createElement("a"); + link.href = `#${symbol.id}`; + link.textContent = symbol.label; + item.append(link); + results.append(item); + } + } + + input.addEventListener("input", search); + form.addEventListener("submit", (event) => { + event.preventDefault(); + search(); + }); + // Keep declaration and contents jumps together with keyboard focus. + document.querySelector("article").addEventListener("click", (event) => { + const link = event.target.closest('a[href^="#"]'); + if (!link || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return; + const target = document.getElementById(decodeURIComponent(link.hash.slice(1))); + if (!target) return; + event.preventDefault(); + history.pushState(null, "", link.hash); + target.setAttribute("tabindex", "-1"); + target.focus({ preventScroll: true }); + target.scrollIntoView({ behavior: "instant", block: "start" }); + }); + search(); +}); diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 0000000..af2c76b --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,221 @@ +/* Extend Furo with the README banner's blue, cyan, and navy palette and task navigation. */ +body { + --font-stack: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-stack--monospace: "SFMono-Regular", Consolas, "Liberation Mono", monospace; +} +article h1 { font-size: 2rem; line-height: 1.25; letter-spacing: -0.025em; } +article h2 { letter-spacing: -0.02em; } +/* Shift subheadings down one step in Furo's type scale. */ +article h2 { font-size: 1.4rem; } +article h3 { font-size: 1.15rem; } +article h4 { font-size: 1.125em; } +article h5 { font-size: 1em; } +article h6 { font-size: .875em; } +.serialxml-banner { + display: block; + width: 100%; + height: auto; + background: #fff; + border-radius: 6px; + border: 1px solid var(--color-background-border); + margin-bottom: 1.2rem; +} +.serialxml-badges { display: flex; flex-wrap: wrap; gap: .55rem; margin: 1.25rem 0; } +.serialxml-badges span { + color: var(--color-brand-content); + border: 1px solid var(--color-background-border); + background: var(--color-sidebar-background); + border-radius: 999px; + padding: .3rem .7rem; + font-size: .8rem; + font-weight: 600; +} +.highlight { border-radius: 6px; background: var(--color-background-secondary); } +.highlight pre { background: transparent; line-height: 1.6; } +article { line-height: 1.65; } +article a, article a:visited { + color: var(--color-brand-content); + text-decoration: none; + border-bottom: 0; +} +article a:hover, article a:focus-visible { + color: var(--color-brand-content); + text-decoration: underline solid; + text-decoration-thickness: 1px; + text-underline-offset: .2em; +} +article code.literal { + border: 0; + background: var(--color-background-secondary); + padding: .12em .3em; + border-radius: 3px; +} +.code-block-caption { + background: var(--color-background-secondary); + border: 0; + padding: .65rem 1rem; + font-size: .8rem; + color: var(--color-foreground-secondary); +} +table.docutils { width: 100%; } +.api-search { + border: 1px solid var(--color-background-border); + border-radius: 8px; + padding: 1rem; + margin: 1.5rem 0; +} +.api-search label { display: block; font-weight: 600; margin-bottom: .5rem; } +.api-search input { + box-sizing: border-box; + width: 100%; + font: inherit; + color: var(--color-foreground-primary); + background: var(--color-background-primary); + border: 1px solid var(--color-background-border); + border-radius: 4px; + padding: .6rem; +} +.api-search button { + font: inherit; + color: var(--color-brand-content); + background: var(--color-sidebar-background); + border: 1px solid var(--color-background-border); + border-radius: 4px; + padding: .4rem .8rem; + margin-top: .5rem; + cursor: pointer; +} +.api-search input:focus-visible, .api-search button:focus-visible { + outline: 2px solid var(--color-brand-primary); + outline-offset: 2px; +} +#api-search-status { font-size: .875rem; margin-bottom: 0; } +#api-search-results { max-height: 18rem; overflow-y: auto; padding-left: 1.25rem; } +#api-search-results a { overflow-wrap: anywhere; } +article .sig[id], article section[id] { scroll-margin-top: 4rem; } +@media (max-width: 640px) { + .serialxml-badges { gap: .35rem; } +} + +/* Section switching and location context remain available without JavaScript. */ +.section-nav { + display: flex; + flex-wrap: wrap; + gap: .35rem; + padding: .2rem 0 1rem; + margin: 0 0 1.25rem; + border-bottom: 1px solid var(--color-background-border); + clear: both; +} +.section-nav a { + font-size: .82rem; + font-weight: 500; + padding: .45rem .65rem; + border-radius: 0; + color: var(--color-foreground-secondary); + text-decoration: none; +} +.section-nav a:hover, +.section-nav a[aria-current="page"], +.section-nav a.active-section { + background: transparent; + color: var(--color-brand-content); +} +.section-nav a[aria-current="page"], .section-nav a.active-section { + box-shadow: inset 0 -2px var(--color-brand-primary); +} +.breadcrumbs { font-size: .75rem; color: var(--color-foreground-secondary); } +.breadcrumbs a, .breadcrumbs a:visited { color: var(--color-foreground-secondary); } +.breadcrumbs ol { list-style: none; padding: 0; display: flex; flex-wrap: wrap; gap: .4rem; } +.breadcrumbs li { margin: 0; overflow-wrap: anywhere; } +.breadcrumbs li + li::before { content: "/"; margin-right: .4rem; opacity: .6; } +.hero-kicker { + margin-top: 1.5rem; + font-size: .8rem; + text-transform: uppercase; + letter-spacing: .12em; + font-weight: 700; + color: var(--color-brand-content); +} +.hero-kicker + section > h2 { font-size: 2.25rem; line-height: 1.15; margin-top: .75rem; } +.hero-actions { display: flex; flex-wrap: wrap; gap: .65rem; margin: 1.5rem 0; } +.hero-actions a { padding: .65rem 1rem; border-radius: 5px; font-weight: 600; text-decoration: none; } +.hero-actions .primary-action { color: #fff; background: #245fd1; } +.hero-actions .primary-action:hover { background: #1d51b8; } +.hero-actions .secondary-action { color: var(--color-brand-content); border: 1px solid var(--color-background-border); } +.doc-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; margin: 1.5rem 0 2rem; } +a.doc-card { + display: flex; + flex-direction: column; + gap: .6rem; + padding: 1.25rem; + border: 1px solid var(--color-background-border); + border-radius: 6px; + background: var(--color-background-secondary); + color: var(--color-foreground-primary); + text-decoration: none; + font-size: .9rem; + line-height: 1.55; +} +a.doc-card:hover { border-color: var(--color-brand-primary); } +.doc-card strong { font-size: 1.25rem; letter-spacing: -.02em; } +.card-kind { font-size: .7rem; text-transform: uppercase; letter-spacing: .09em; color: var(--color-foreground-secondary); font-weight: 700; } +.card-link { color: var(--color-brand-content); font-weight: 600; margin-top: auto; padding-top: .5rem; } +article a:focus-visible { outline: 3px solid var(--color-brand-primary); outline-offset: 3px; border-radius: 3px; } +.sidebar-tree .reference { overflow-wrap: anywhere; } +@media (max-width: 640px) { + .doc-grid { grid-template-columns: 1fr; } + .section-nav { gap: .2rem; } + .section-nav a { padding: .5rem; } + .hero-kicker + section > h2 { font-size: 1.8rem; } +} + +/* Navigation uses color and the active rule rather than decorative underlines. */ +article .section-nav a:hover, article .breadcrumbs a:hover { + text-decoration: none; + color: var(--color-brand-content); +} +article .hero-actions a:hover, article a.doc-card:hover { text-decoration: none; } +article .hero-actions .primary-action, +article .hero-actions .primary-action:visited, +article .hero-actions .primary-action:hover { color: #fff; } +article a.doc-card, article a.doc-card:visited { color: var(--color-foreground-primary); } +@media (max-width: 640px) { + article h1 { font-size: 1.75rem; } +} + +/* A visible starting point and an ordered learning path for tutorials. */ +.tutorial-start { + padding: 1.25rem 1.5rem; + margin: 1.5rem 0 2rem; + border-left: 3px solid var(--color-brand-primary); + background: var(--color-background-secondary); + border-radius: 0 6px 6px 0; +} +.tutorial-start p { margin: 0 0 .75rem; } +.tutorial-start-link { font-weight: 600; } +.tutorial-path { list-style: none; padding: 0; margin: 1.5rem 0 2rem; } +.tutorial-path li { margin: 0; border-top: 1px solid var(--color-background-border); } +.tutorial-path li:last-child { border-bottom: 1px solid var(--color-background-border); } +article .tutorial-path a { + display: flex; + align-items: flex-start; + gap: 1rem; + padding: 1.25rem .75rem; + color: var(--color-foreground-primary); +} +article .tutorial-path a:visited { color: var(--color-foreground-primary); } +article .tutorial-path a:hover { + text-decoration: none; + background: var(--color-background-secondary); +} +.lesson-number { color: var(--color-brand-content); font: 600 .85rem var(--font-stack--monospace); padding-top: .25rem; } +.lesson-content { display: flex; flex-direction: column; gap: .35rem; flex: 1; min-width: 0; } +.lesson-content strong { font-size: 1.1rem; } +.lesson-content > span { font-size: .9rem; } +.lesson-content .lesson-skills { font-size: .75rem; color: var(--color-foreground-secondary); } +.lesson-arrow { color: var(--color-brand-content); padding-top: .1rem; } +@media (max-width: 640px) { + .tutorial-start { padding: 1rem; } + article .tutorial-path a { padding: 1rem .25rem; gap: .75rem; } +} diff --git a/docs/_templates/page.html b/docs/_templates/page.html new file mode 100644 index 0000000..ed7d4cf --- /dev/null +++ b/docs/_templates/page.html @@ -0,0 +1,20 @@ +{% extends "!page.html" %} +{% block content %} + +{% if pagename != "index" %} + +{% endif %} +{{ super() }} +{% endblock %} diff --git a/docs/annotations.md b/docs/annotations.md new file mode 100644 index 0000000..6aab85a --- /dev/null +++ b/docs/annotations.md @@ -0,0 +1,73 @@ +# Annotation reference + +This library is annotation driven. Most customization points are exposed via +C++26 annotations, so the XML structure stays visually associated with the C++ +definition. + +By default, members become child elements named after the member. Classes that +cannot be formatted are unpacked into their own members. Many STL ranges and +`std::optional` are handled automatically; an empty `std::optional` is omitted. + +For example, here's how to make `age` an attribute and rename `favorite_food`: + +```cpp +struct Person { + [[= serial_xml::attribute]] int age; + [[= serial_xml::name{"food"}]] std::string favorite_food; +}; + +const auto xml = serial_xml::to_xml(Person{3, "pizza"}, false); +// pizza +``` + +The annotations below live in the `serial_xml` namespace. Use the qualified name, +as above, or bring the annotations into scope. + +| Annotation | What it does | Details | +| --- | --- | --- | +| `[[=attribute]]` | Emit a member as an XML attribute. | Uses the member name unless overridden by `name`. | +| `[[=name{"custom_name"}]]` | Rename an attribute or element. | Also works on a class or struct to rename its root element. | +| `[[=skip]]` | Omit a member. | Ignored by serialization and deserialization. | +| `[[=raw]]` | Emit text without the member's enclosing tag. | For ranges, removes the outer range tag, not the item tags. Text is still XML-escaped. | +| `[[=cdata]]` | Emit the value in a CDATA section. | Produces ``. | +| `[[=unpack]]` | Serialize an object's members inside an enclosing element. | Uses reflection instead of `std::format`. | +| `[[=no_unpack]]` | Format an object as text. | Disables unpacking. | +| `[[=iter{a, b}]]` | Iterate a range instead of formatting it as text. | Optional `a` names each item; optional `b` names the enclosing range tag. | +| `[[=no_iter]]` | Disable automatic range iteration. | Uses the range's formatted representation. | +| `[[=exclude_on_empty]]` | Omit tags for an empty range. | See the precedence rules below. | +| `[[=format{"format_specifier"}]]` | Pass a format specifier to `std::format`. | Leave out the leading `:`; SerialXML adds it. | +| `[[=format{format_function}]]` | Use a custom formatting function. | Accepts the member value and returns a string-like value. | +| `[[=optional]]` | Allow a field or selected setter to be absent when reading. | An absent member keeps its initializer or current value. | +| `[[=setter]]` | Select a one-parameter method for XML input. | Ignored by `to_xml`; see [Setters and encapsulation](deserialization.md#read-through-setters). | + +## Precedence and automatic handling + +Some STL containers are automatically iterated. If you want their formatted text +instead, add `[[=serial_xml::no_iter]]` to the member. The automatically iterated +containers are `std::vector`, `std::array`, `std::inplace_vector`, `std::deque`, +`std::forward_list`, `std::span`, and `std::valarray`. + +When annotations overlap, precedence matters: + +| Context | Precedence (highest first) | +| --- | --- | +| Automatically handled STL ranges | `exclude_on_empty` → `raw` → `cdata` | +| Child members | STL handling → `raw` → iteration → unpacking → `cdata` | + +For ranges, `raw` only removes the outer layer of tags. Individual elements keep +their tags. `format` is ignored for unpacked or iterated members. + +## Recipes by annotation + +| Annotation | Task guide | +| --- | --- | +| `attribute` | [Put a value in an attribute](how-to/attributes.md) | +| `name` | [Match an XML naming scheme](how-to/naming.md) | +| `skip` | [Leave a field out of the schema](how-to/skip-fields.md) | +| `iter`, `no_iter`, `exclude_on_empty` | [Choose a container representation](how-to/containers.md) · [Name items and wrappers](how-to/item-names.md) | +| `raw`, `cdata` | [Write direct text or CDATA](how-to/text-and-cdata.md) | +| `format` | [Format numbers](how-to/numeric-formatting.md) · [Round-trip custom text](how-to/custom-conversion.md) | +| `optional` | [Retain defaults for missing fields](deserialization.md#retain-defaults-when-a-field-is-missing) | +| `setter` | [Read private state through setters](deserialization.md#read-through-setters) | + +For constructors and exported marker declarations, see the [API reference](api.rst). diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..126a742 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,344 @@ +API reference +============= + +Import the C++26 module with ``import serial_xml;``. All exported symbols live in +the ``serial_xml`` namespace. This reference describes the public declarations in +``include/serial_xml.cxx``; reflection helpers and parser internals are not public API. + +For procedures, use :doc:`serialization` and :doc:`deserialization`. For the +complete annotation table and precedence, use :doc:`annotations`. + +.. list-table:: Entry points + :header-rows: 1 + :widths: 45 55 + + * - Task + - Symbol + * - Write a document or fragment + - :cpp:func:`serial_xml::to_xml` + * - Read a new value or update an existing object + - :cpp:func:`serial_xml::from_xml` + * - Convert leaf text or provide a custom conversion + - :cpp:func:`serial_xml::from_string` + * - Format XML for display + - :cpp:func:`serial_xml::prettify` + * - Catch a built-in read failure + - :cpp:class:`serial_xml::deserialization_error` + +.. raw:: html + + + +.. contents:: Table of contents + :local: + :depth: 1 + :class: this-will-duplicate-information-and-it-is-still-useful-here + +.. cpp:namespace:: serial_xml + +Serialization +------------- + +.. cpp:function:: template std::string to_xml(const T& value, bool first = true, const std::string& fixed_name = "") + + Serialize a class instance into compact UTF-8 XML. + + ``T`` must be a class type; ``Schema`` must be ``void`` or a class type. + With ``Schema = void``, reflection uses ``T`` and its annotations. An external + schema selects and annotates accessible fields and const getters by identifier. + The schema itself is never constructed. Nested objects use their own schemas. + + :param value: Object to read. Serialization does not modify it. + :param first: Include ```` when true. + :param fixed_name: Explicit root element name. An empty string uses a schema + ``name`` annotation, then the actual type's identifier. + :returns: The XML document or fragment. + :throws std\:\:invalid_argument: An invalid XML name, XML character, or UTF-8 + sequence is encountered. + + Invalid annotation combinations and unresolved or ambiguous schema members are + diagnosed at compile time. Formatting exceptions and exceptions from getters or + custom formatter functions propagate to the caller. + + .. code-block:: cpp + + auto document = serial_xml::to_xml(person); + auto fragment = serial_xml::to_xml(person, false, "person"); + auto custom = serial_xml::to_xml(person); + +Deserialization +--------------- + +.. cpp:function:: template T from_xml(std::string_view xml, const std::string& fixed_name = "") + + Value-initialize ``T``, read XML into it, and return the result. + + ``T`` must be a default-initializable class type. With ``Schema = void``, + reflection uses ``T``; otherwise an external schema selects input members and + provides their annotations. The expected root name follows the same rules as + :cpp:func:`to_xml`. + + :param xml: Complete UTF-8 XML input, with or without a declaration. + :param fixed_name: Override the expected root name. + :returns: A populated instance of ``T``. + :throws deserialization_error: Malformed XML, a mismatched root, missing required + members, duplicate selected elements, or invalid built-in conversions. + :throws std\:\:invalid_argument: The expected root name is not a valid XML name. + + Exceptions from custom conversions and setters propagate. Read-only getters are + ignored; writable fields and getters with matching setters receive values. + Extra object elements and attributes are ignored; iterated range contents are + checked against the item name. + + .. code-block:: cpp + + auto person = serial_xml::from_xml(xml); + auto custom = serial_xml::from_xml(xml, "person"); + +.. cpp:function:: template void from_xml(T& result, std::string_view xml, const std::string& fixed_name = "") + + Read XML into an existing instance. ``T`` is deduced, so an explicit template + argument selects the schema: ``from_xml(person, xml)``. + + ``T`` must be a class type and ``T&`` must not be convertible to + ``std::string_view``. Unlike the value-returning overload, this overload does + not require default construction of the target object. + + :param result: Object to update. + :param xml: Complete UTF-8 XML input. + :param fixed_name: Override the expected root name. + + The input is copied before parsing, so it may refer to a string owned by + ``result``. XML is fully parsed before member assignments. A conversion, + assignment, or setter failure can leave ``result`` partially updated. An absent + member annotated with :cpp:var:`optional` keeps its current value. + The exception behavior is the same as the value-returning overload. + +.. cpp:function:: template T from_string(std::string_view text) + + Convert the decoded text of a scalar XML member to ``T``. This function also + serves as the customization point for leaf types. + + :param text: Scalar text after XML entity decoding and XML newline normalization. + Calling this function directly does not parse XML or decode entities. + :returns: The converted value. + :throws deserialization_error: The built-in converter cannot parse the complete + representation, a number is out of range, or a container has the wrong size. + + Built-in conversions support: + + * ``std::string``, retaining whitespace. + * ``bool``: ``true``, ``false``, ``1``, and ``0`` after trimming XML whitespace. + * Arithmetic types via ``std::from_chars`` after trimming XML whitespace. + ``char`` therefore uses its numeric representation. + * ``std::optional``, constructing an engaged optional from ``T``'s conversion. + * Tuple-like types such as pairs and tuples in ``(value, value)`` form. + * Owning, writable sequences in ``[value, value]`` form, sets in + ``{value, value}`` form, and maps in ``{key: value}`` form. + + Nested containers and quoted strings use the standard formatting representation. + Fixed-size arrays must receive exactly their size; fixed-capacity containers + reject too many items. Views such as ``std::span`` are not owning input targets. + Unsupported leaf types produce a compile-time diagnostic requesting a + specialization. + + Supply a specialization for a custom formatted leaf. It receives the formatted + text; a ``format`` annotation is not automatically inverted during input: + + .. code-block:: cpp + + struct Code { + int value; + }; + + template <> + Code serial_xml::from_string(std::string_view text) { + return {serial_xml::from_string(text)}; + } + + See :doc:`deserialization` for complete custom-format and setter examples. + +.. cpp:class:: deserialization_error : public std::runtime_error + + Exception for XML parsing, input schema checks, and built-in text conversion + failures. Inherits ``std::runtime_error`` constructors and ``what()``. + Catch it to obtain the diagnostic message: + + .. code-block:: cpp + + try { + auto person = serial_xml::from_xml(xml); + } catch (const serial_xml::deserialization_error& error) { + std::println("{}", error.what()); + } + +Pretty printing +--------------- + +.. cpp:function:: std::string prettify(const std::string& xml) + + Validate XML and return a readable representation using two-space indentation + for element-only content. Text-bearing subtrees, including whitespace and + CDATA, and subtrees with an ``xml:space`` attribute retain their original bytes. + + :param xml: A complete XML document or single-root fragment. + :returns: Formatted XML. No final newline is added automatically. + :throws deserialization_error: The input is malformed or unsupported by the XML parser. + + Inserted whitespace becomes text when reparsed. Use compact :cpp:func:`to_xml` + output when round-tripping raw scalar members alongside child elements or when + every whitespace node matters. + +Annotation value types +---------------------- + +These structural values are used in C++26 annotations such as +``[[= serial_xml::name{"person"}]]``. Array lengths include the null terminator; +class template argument deduction supplies them from string literals. + +.. cpp:struct:: template name + + Override a member's XML element or attribute name, or a class/schema root name. + + .. cpp:var:: char value[N] + + The supplied null-terminated XML name, stored inline. + + .. cpp:function:: constexpr name(const char (&str)[N]) + + Copy and validate ``str`` as an XML name. Empty names are invalid. Invalid + names or UTF-8 throw ``std::invalid_argument`` during construction; in a + constant annotation this prevents compilation. + + .. cpp:function:: static constexpr bool is_empty() + + Return whether ``N == 1``. This reports the template's array length; + construction still rejects an empty name. + +.. cpp:struct:: template format + + Specify a standard format specifier or a custom formatter function. + + .. cpp:var:: char value[N] = {} + + Inline format-specifier storage. Use a specifier without ``:`` or braces; + the serializer builds ``{:specifier}``. + + .. cpp:var:: F function = {} + + Callable storage for function-based formatting. ``std::nullptr_t`` selects + the string-specifier form. + + .. cpp:function:: constexpr format(const char (&str)[N]) + + Available only when ``F`` is ``std::nullptr_t``. Copy the format specifier, + for example ``format{".2f"}``. The empty specifier uses default formatting. + + .. cpp:function:: constexpr format(F formatter) + + Available only when ``F`` is not ``std::nullptr_t``. Store a callable that + accepts the member value and returns a value constructible as + ``std::string``. For example, ``format{[](int n) { return std::to_string(n); }}``. + Function-based formatting disables automatic object unpacking. + + Formatting affects output. Input uses :cpp:func:`from_string` and may need a + matching specialization. See :doc:`annotations` for interaction with iteration. + +.. cpp:struct:: template iter + + Iterate a range with configurable item and wrapper names. Applicable only to + range members. + + .. cpp:var:: char single[N1] = "" + + Item element name. An empty string selects ``element``. + + .. cpp:var:: char multiple[N2] = "" + + Wrapper element name. An empty string uses the member identifier, or + ``elements`` when an identifier is unavailable. + + .. cpp:function:: constexpr iter(const char (&s)[N1]) + + Set the item name, leaving the wrapper name at its default. + + .. cpp:function:: constexpr iter(const char (&s)[N1], const char (&m)[N2]) + + Set both names, for example ``iter{"person", "people"}``. Each nonempty + name must be a valid XML name; invalid names throw ``std::invalid_argument`` + during construction. Empty strings request the defaults. + + There is no zero-argument constructor. Use ``iter{""}`` for default names. + ``raw`` removes the wrapper, while keeping item elements. + +Annotation markers +------------------ + +Each constant is an exported value of a distinct marker type. Use the constants +directly in annotations; the marker types are implementation details. See +:doc:`annotations` for valid combinations and precedence. + +.. cpp:var:: constexpr attribute_ attribute + + Write/read a member as an XML attribute instead of a child element. + ``name`` can override the attribute name. Attributes cannot be combined with + ``raw``, ``cdata``, or object unpacking. + +.. cpp:var:: constexpr skip_ skip + + Exclude a member from both serialization and deserialization. A skipped schema + placeholder does not need a matching target member. + +.. cpp:var:: constexpr raw_ raw + + Omit a scalar member's element tags and write escaped text into the parent. + For iterated ranges, omit the wrapper while retaining item tags. This does not + insert unescaped XML. Multiple raw scalar input members cannot be separated. + +.. cpp:var:: constexpr cdata_ cdata + + Emit formatted text directly into the parent as CDATA. The serializer splits + ``]]>`` safely and preserves carriage returns using character references. + Input consumes CDATA fields in member order. Default arithmetic conversion + takes precedence and keeps ordinary tagged numeric output. + +.. cpp:var:: constexpr unpack_ unpack + + Reflect an object's members inside its enclosing element rather than formatting + it as scalar text. Non-formattable class members are unpacked automatically. + +.. cpp:var:: constexpr no_unpack_ no_unpack + + Disable automatic object unpacking and use formatted text. Input requires a + compatible :cpp:func:`from_string` conversion. + +.. cpp:var:: constexpr no_iter_ no_iter + + Disable automatic iteration of supported standard containers and use their + formatted representation instead. Input converts that representation through + :cpp:func:`from_string`. + +.. cpp:var:: constexpr exclude_on_empty_ exclude_on_empty + + Omit empty automatically handled range output. Without this marker, an empty + range emits a self-closing wrapper. When reading an absent automatically + iterated range with this marker, value-initialize the container (empty for dynamic ranges). + +.. cpp:var:: constexpr optional_ optional + + Allow an input field or selected setter to be absent. Leave its initializer or + existing value unchanged, and do not invoke an absent setter. This marker does + not change serialization and is distinct from a ``std::optional`` member. + +.. cpp:var:: constexpr setter_ setter + + Select a non-static, one-parameter method for deserialization. Ignored during + serialization. Without a ``name`` override, ``set_name`` reads ``name`` and + ``setName`` reads ``name``. See :doc:`deserialization` for automatic getter/setter + matching and schema setters. diff --git a/docs/benchmarking.md b/docs/benchmarking.md new file mode 100644 index 0000000..117ec1c --- /dev/null +++ b/docs/benchmarking.md @@ -0,0 +1,68 @@ +# Run the benchmarks + +Measure serialization and deserialization on the repository’s order workload. +The executable verifies round trips before timing and reports timings per order. +Use a release build so compiler optimization is enabled. + +## Prepare the build + +Use the dev container’s GCC and CMake toolchain. Install Boost Serialization and +cereal in that environment; the benchmark configuration fetches pugixml 1.16. +From the repository root, configure the release build with tests and benchmarks: + +```bash +cmake --preset release-test-gcc -DBUILD_BENCHMARKS=ON +``` + +If configuration cannot find a required dependency, install it before continuing. +Disabling benchmarks would omit the executable you need for this task. + +## Build and validate the workload + +```bash +cmake --build --preset build-release-test +ctest --test-dir build/release --output-on-failure +``` + +Continue after the build and tests succeed. The benchmark also checks its own +round-trip results before recording measurements, so an invalid conversion is +reported before timing begins. + +## Run a measurement + +Run without CPU pinning first: + +```bash +./build/release/benchmarks/xml_serialization_benchmarks 100000 1000 +``` + +The first argument requests 100,000 measured iterations; the second requests +1,000 warm-up iterations. These are also the defaults if you omit both arguments. +Output compares SerialXML, Boost.Serialization, cereal, and pugixml for the same +logical order workload. Smaller nanoseconds-per-order values indicate less time +spent per operation. + +## Repeat with a fixed CPU + +On Linux, find the CPUs available to your process: + +```bash +taskset -pc $$ +``` + +Choose a CPU from that list. For example, if CPU 4 is available: + +```bash +for run in 1 2 3 4 5; do + taskset -c 4 ./build/release/benchmarks/xml_serialization_benchmarks 100000 1000 +done +``` + +Run the measurements sequentially and compare the median of each library’s five +results. Record the CPU model, compiler version, build options, and iteration +counts with your measurements. Hardware, compiler settings, and XML shape affect +both absolute timings and relative performance. + +The libraries emit different archive metadata and container names. Compare time +per logical order rather than XML bytes per second. The [recorded benchmark results](benchmarks.md) +provide the repository’s workload and environment for comparison. diff --git a/docs/benchmarks.md b/docs/benchmarks.md new file mode 100644 index 0000000..86b32f3 --- /dev/null +++ b/docs/benchmarks.md @@ -0,0 +1,26 @@ +# Benchmarks + +The repository compares SerialXML with Boost.Serialization, cereal, and pugixml +using an order with customer details, a shipping address, line items, and integer +tags. The benchmark checks round trips before timing. Deserialization includes +parsing, numeric conversion, and owning object construction. + +## Recorded results + +In the order benchmark reported in the project README, SerialXML deserialization +was **3.8× faster than cereal, 16.8× faster than Boost, and 9% faster than pugixml**. + +| Library | Serialization (ns/order) | Deserialization (ns/order) | +| --- | ---: | ---: | +| **SerialXML** | **830.308** | **849.361** | +| Boost.Serialization | 6959.951 | 14250.021 | +| cereal | 7699.705 | 3195.171 | +| pugixml | 1523.398 | 936.498 | + +Lower is better. These measurements were recorded on an Intel Core 7 240H with +GCC 16.2.0. They describe this workload and environment; timings and relative +performance can change with hardware, compiler settings, data, and XML structure. +The libraries produce different incidental archive metadata and container names, +so compare logical orders per second rather than XML bytes per second. + +For build commands and CPU pinning, follow [Run the benchmarks](benchmarking.md). diff --git a/docs/building-docs.md b/docs/building-docs.md new file mode 100644 index 0000000..d801941 --- /dev/null +++ b/docs/building-docs.md @@ -0,0 +1,75 @@ +# Building and publishing the documentation + +This site uses [Sphinx](https://www.sphinx-doc.org/), +[MyST](https://myst-parser.readthedocs.io/) for Markdown, and the +[Furo theme](https://furo.readthedocs.io/). It builds without the C++ toolchain. +Python 3.13 is used by Read the Docs and documentation CI. + +## Build locally + +Run these commands from the repository root: + +```bash +python3 -m venv build/docs-venv +build/docs-venv/bin/python -m pip install -r docs/requirements.txt +build/docs-venv/bin/python -m sphinx -b html -n -W --keep-going docs build/docs/html +python3 -m http.server 8000 --directory build/docs/html +``` + +Open . Search, the symbol index, source links, and the +light/dark theme toggle are available in the generated site. Generated HTML and +the virtual environment stay under the ignored `build/` directory. + +`-n -W --keep-going` checks references and treats warnings as build failures. +The documentation workflow runs the same strict build for relevant pull requests. + +## Connect Read the Docs + +1. Push the documentation and root `.readthedocs.yaml` to GitHub. +2. Import `EJainDev/SerialXML` in your Read the Docs account and select the branch + containing these files. +3. Trigger a build. The supplied configuration uses Ubuntu 24.04 and Python 3.13, + installs `docs/requirements.txt`, and builds `docs/conf.py` with warnings treated + as errors. +4. Set `READTHEDOCS_URL` near the bottom of `docs/conf.py` to your final documentation + URL, for example `https://YOUR-PROJECT.readthedocs.io/en/latest/`. Hosted builds + automatically use the canonical URL provided by Read the Docs. +5. Add the public URL to the repository's GitHub website field and README when it + is available. + +See the [Read the Docs configuration reference](https://docs.readthedocs.io/en/stable/config-file/v2.html) +for supported hosting options. The public URL is optional for local builds. + +## Maintain the content + +- The site follows [Diátaxis](https://diataxis.fr/). Put guided learning in + `tutorials/`, task recipes in `how-to/`, technical contracts in reference pages, + and conceptual discussion in `explanation/`. Each section has an `index.md` + landing page; add new pages to exactly one section's toctree so breadcrumbs, + sidebar context, and previous/next links agree. +- Keep existing page URLs and section anchors working when reorganizing content. + `examples.md` retains the original cookbook anchors and links to the new recipes. +- Tutorials should include prerequisites, complete code, expected output, and a + next step. Programs and checkpoints for the project lessons live in + `_snippets/tutorials/`; compile and run each checkpoint when editing a lesson. How-to guides should begin with a task and link to the relevant + reference rather than repeat its contract. +- `api.rst` is an indexed, curated reference to the exported symbols in + `include/serial_xml.cxx`. Update its signatures and behavior descriptions when + the public API changes. The reference does not require a reflection-aware + Doxygen parser or a C++ compilation step. +- Recipes under `how-to/` include complete, focused programs from + `_snippets/how-to/` in portions using `literalinclude`. Keep the imports, + declarations, and complete `main` visible; compile each program and compare its + output with the documented result after edits. If formatting changes line + positions, update the included ranges too. +- `examples.md` includes the repository's actual hello-world source. Shared CMake + setup lives in `_snippets/CMakeLists.txt` to keep installation and the tutorial + aligned. +- The homepage reuses `assets/SerialXML.png`, the README banner. Theme colors + in `conf.py` and button accents in `_static/custom.css` follow its blue, cyan, + and navy palette. +- `_templates/page.html` adds section navigation and breadcrumbs to Furo. + `_static/custom.css` styles the landing cards, navigation, and light/dark themes. + Keep navigation usable without JavaScript and test narrow screens and keyboard focus. +- `requirements.txt` pins the documentation dependencies and their dependencies. + Update and verify them together when upgrading Sphinx or the theme. diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..fa446ea --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,80 @@ +"""Sphinx configuration for SerialXML's Read the Docs website.""" + +import os +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent / "_ext")) + +project = "SerialXML" +author = "SerialXML contributors" +html_show_copyright = False +version = "1.0" +release = "1.0.0" + +extensions = ["myst_parser", "cpp26"] +source_suffix = {".rst": "restructuredtext", ".md": "markdown"} +root_doc = "index" +exclude_patterns = ["README.md", "_snippets"] +templates_path = ["_templates"] +myst_heading_anchors = 3 +highlight_language = "cpp" +pygments_style = "friendly" +pygments_dark_style = "github-dark" +nitpicky = True +# The standard library is external, and marker backing types are intentionally opaque. +# Keep checks enabled for all actual SerialXML public symbols. +nitpick_ignore_regex = [("cpp:identifier", r"std::.*")] +nitpick_ignore = [ + ("cpp:identifier", marker + "_") + for marker in ( + "attribute", "skip", "raw", "cdata", "unpack", "no_unpack", "no_iter", + "exclude_on_empty", "optional", "setter", + ) +] + +html_theme = "furo" +html_title = "SerialXML documentation" +html_static_path = ["_static"] +html_css_files = ["custom.css"] +html_js_files = ["api-search.js"] +html_theme_options = { + # Blue/cyan accents and navy text echo the README banner. Link colors use + # darker/lighter variants to retain contrast in each theme. + "light_css_variables": { + "color-brand-primary": "#3679f5", + "color-brand-content": "#245fd1", + "color-foreground-primary": "#0b1425", + "color-foreground-secondary": "#536078", + "color-background-primary": "#ffffff", + "color-background-secondary": "#f5f9ff", + "color-background-border": "#d8e4f4", + "color-sidebar-background": "#f5f9ff", + "color-sidebar-link-text": "#536078", + }, + "dark_css_variables": { + "color-brand-primary": "#6cbdff", + "color-brand-content": "#82c4ff", + "color-foreground-primary": "#e8effb", + "color-foreground-secondary": "#b0bfd6", + "color-background-primary": "#0b1425", + "color-background-secondary": "#14223b", + "color-background-border": "#2a3d5b", + "color-sidebar-background": "#101c30", + "color-sidebar-link-text": "#b0bfd6", + }, + "source_repository": "https://github.com/EJainDev/SerialXML/", + "source_branch": "main", + "source_directory": "docs/", +} + +# Fill this in after importing the repository into Read the Docs. +# Read the Docs supplies its canonical URL automatically during hosted builds. +READTHEDOCS_URL = "" +html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", READTHEDOCS_URL) +if os.environ.get("READTHEDOCS") == "True": + html_context = { + "READTHEDOCS": True, + "READTHEDOCS_PROJECT": os.environ.get("READTHEDOCS_PROJECT", ""), + "READTHEDOCS_VERSION": os.environ.get("READTHEDOCS_VERSION", ""), + } diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..a4e162e --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,103 @@ +# Contributing + +Thank you for your interest in contributing to SerialXML! All contributions are greatly appreciated. This document outlines the guidelines for contributing to the project. + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [Reporting Bugs](#reporting-bugs) + - [Compile Errors](#compile-errors) + - [Runtime Bugs](#runtime-bugs) +- [Feature Requests](#feature-requests) +- [Local Setup Instructions](#local-setup-instructions) +- [Testing Guidelines](#testing-guidelines) +- [Style & Commit Rules](#style--commit-rules) +- [Pull Request Guidelines](#pull-request-guidelines) +- [Review Process](#review-process) + +## Code of Conduct + +See [Code of Conduct](https://github.com/EJainDev/SerialXML/blob/main/CODE_OF_CONDUCT.md). + +## Reporting Bugs + +Before submitting a bug report, ensure the issue hasn't been reported already. Provide clear, concise details to help maintainers reproduce and fix the issue. + +### Compile Errors + +To report compile errors, specify: +1. The exact error message or symptom. +2. The compiler and version (for example, GCC 16.1) and the CMake version used (for example, 4.4.3). +3. The full compiler output / compilation log. +4. A minimal, self-contained reproducible example (smallest code that reproduces the problem). +5. The exact build commands or CMake preset used to reproduce the issue (for example: `cmake --preset debug-test-gcc` and `cmake --build --preset build-debug-test`). + +### Runtime Bugs + +To report runtime bugs, specify: +1. What happened. +2. What should have happened. +3. A minimal reproducing example. +4. Why it could have happened (optional). +5. Steps to reproduce the behavior (exact commands, inputs, and any environment information required to trigger the issue). + +## Feature Requests + +Feature requests drive the growth of this library. All requests are welcome, even if you cannot implement them yourself. The feature request template includes a "Testing Commitment" checkbox — please select one of the following options to indicate your level of involvement: + +- **Test only**: I can help test or validate a PR but will not implement it. +- **Implement and test (PR)**: I will implement the feature and submit a PR that includes tests. +- **Neither**: I cannot implement or test this change; this is a request only. + +When possible, prefer **Implement and test (PR)** so the change can be reviewed and merged more quickly. Include: +1. Feature description. +2. Intent and use case for the feature. +3. Example usage (optional). + +## Local Setup Instructions + +1. Clone the repository. +2. Install GCC 16.1 and CMake 4.3.3 or higher. +3. Configure the project using the `debug-test-gcc` preset: `cmake --preset debug-test-gcc` +4. Build the project using the `build-debug-test` preset: `cmake --build --preset build-debug-test` + +## Testing Guidelines + +- Use Google Test as the testing framework. +- Run the full test suite before submitting changes: `ctest --test-dir build/debug --output-on-failure --no-tests=error` +- Check both consumer paths after a build: `cmake -DSERIAL_XML_SOURCE_DIR="$PWD" -DSERIAL_XML_BINARY_DIR="$PWD/build/debug" -P tests/consumer/run.cmake`. +- All tests must pass. Do not break existing functionality. +- Add new tests for new features. +- Add regression tests for bug fixes. +- Verify that changes compile and run correctly across supported platforms. + +## Style & Commit Rules + +- Format code using `clang-format`. Run `clang-format -i ` before committing. +- Follow general guidelines using `clang-tidy`. If you disable a check (globally or locally), explain the rationale in your PR. +- Use conventional commit messages: `(): ` + - `feat`: New feature + - `fix`: Bug fix + - `docs`: Documentation changes + - `style`: Code style changes (formatting, semicolons, etc.) + - `refactor`: Code refactoring + - `test`: Adding or modifying tests + - `chore`: Maintenance tasks + +## Pull Request Guidelines + +- Create a separate branch for each feature or fix. +- Keep PRs focused. One PR per feature/fix. +- Update documentation if API or behavior changes. +- Link related issues in the PR description. +- Ensure all tests pass locally before requesting a review. + +## Review Process + +1. **Local Verification:** Ensure all tests pass and code is formatted correctly. +2. **Maintainer Review:** Maintainers will evaluate functionality, code quality, and alignment with project goals. +3. **Feedback Loop:** Address review comments promptly. Push updates to the same branch. +4. **Merge:** Once approved, a maintainer will squash and merge the PR. +5. **Release:** Changes will be included in the next scheduled release or hotfix. + +Thank you for helping improve SerialXML! \ No newline at end of file diff --git a/docs/deserialization.md b/docs/deserialization.md new file mode 100644 index 0000000..cc97e42 --- /dev/null +++ b/docs/deserialization.md @@ -0,0 +1,134 @@ +# Read XML into objects + +Construct a new object from XML, update an existing object, or load private state +through a setter. The programs below show each task in full, with their imports, +types, and application bodies. + +Use a CMake application linked to `serial_xml::serial_xml`; see +[installation](installation.md) if needed. Save one program at a time as `main.cpp` +and build it with: + +```bash +cmake -S . -B build -G Ninja +cmake --build build +./build/my_app +``` + +## Define the input record + +Start the first program with these imports and declarations: + +```{literalinclude} _snippets/how-to/read-objects.cpp +:language: cpp +:lines: 1-8 +``` + +The reader expects `age` as an attribute and `name` as a child. The nickname can +be omitted because it carries the `optional` annotation. + +## Update an existing object + +Append this complete application body: + +```{literalinclude} _snippets/how-to/read-objects.cpp +:language: cpp +:lines: 10- +``` + +The first `from_xml` constructs a new, value-initialized object. The +in-place call instead takes `person` first and updates it. This form also supports +types without a default constructor. The last read constructs a separate value +before replacing the destination. + +## Retain defaults when a field is missing + +The program prints: + +```text +Alex: age 21; nickname unknown +Sam: age 22; nickname Ace +Replaced: Alex; nickname unknown +``` + +An absent annotated `optional` field retains its initializer in a new object or +its current value in an existing object. A plain `std::optional` instead resets +to `nullopt` when absent. An empty element is present: it can supply an empty +string, but not a valid integer. + +The in-place reader snapshots and parses input before changing members. A later +conversion or setter failure can still leave earlier members updated. When you +need to defer replacement until conversion succeeds, read into a temporary as +shown above. The final assignment follows your type’s own exception guarantees. + +## Convert a custom leaf type + +Built-in leaf conversions cover arithmetic values, owning strings, and supported +owning containers and tuples. When your text has a custom prefix or other +representation, supply a `from_string` specialization before its first use and +pair it with an output formatter. The [custom-conversion guide](how-to/custom-conversion.md) +shows the full program, including validation and error handling. + +A `format` annotation controls output text; the reader cannot infer its inverse. +Borrowed views such as `string_view` and `span` have no built-in reconstruction +because they do not own storage. + +## Read through setters + +For the next program, replace `main.cpp` with these declarations and the +application body below them: + +```{literalinclude} _snippets/how-to/read-setters.cpp +:language: cpp +:lines: 1-13 +``` + +`setter` selects the accessible one-parameter method for input. The explicit +`name` tells it to read the `balance` child. `skip` keeps the getter out of the +mapping so the selected setter handles that input directly. + +```{literalinclude} _snippets/how-to/read-setters.cpp +:language: cpp +:lines: 15- +``` + +After building and running, the program prints: + +```text +Balance: 125 +``` + +You can also rely on automatic getter/setter pairing: `value()` pairs with +`value(T)` or `set_value(T)`, `get_value()` with `set_value(T)`, and `getValue()` +with `setValue(T)`. The value types must match, ignoring references and cv +qualifiers. Getters without a matching setter are ignored during input. Explicitly +selected setters take precedence over automatic pairing and are ignored by +`to_xml`. + +## Diagnose a read failure + +For the final program, define a record with a required numeric child: + +```{literalinclude} _snippets/how-to/read-errors.cpp +:language: cpp +:lines: 1-6 +``` + +Then append the application body: + +```{literalinclude} _snippets/how-to/read-errors.cpp +:language: cpp +:lines: 8- +``` + +The XML is well formed, but its value cannot be converted to an integer. Catching +`deserialization_error` lets the application report the diagnostic: + +```text +Read failed +Invalid or out-of-range numeric value: not-a-number +``` + +The same exception covers malformed XML, mismatched roots, missing required +fields, and failed built-in conversions. Invalid runtime root names throw +`std::invalid_argument`. Exceptions from custom converters and setters propagate +to the caller. diff --git a/docs/design.md b/docs/design.md new file mode 100644 index 0000000..9e72f22 --- /dev/null +++ b/docs/design.md @@ -0,0 +1,76 @@ +# How SerialXML maps C++ to XML + +SerialXML uses C++26 reflection to discover accessible members at compile time. +The resulting mapping connects an object's type to its XML shape. This removes +the need to write a separate serializer for each ordinary struct. + +## Types describe structure; annotations describe intent + +The default mapping uses a type's identifier as the root name and member +identifiers as element names. Accessible const getters can expose values too. +Attributes, skipped fields, and renamed tags are deliberate schema choices, so +annotations attach those choices directly to declarations. + +Classes that cannot be formatted are unpacked into members. A formattable value +can instead become leaf text. `unpack` and `no_unpack` let a schema make this +choice explicitly. This distinction matters for custom types: structured objects +are reconstructed through reflection, while leaves are reconstructed through +`from_string`. + +## Structure and textual formatting are different contracts + +Many common sequences are automatically iterated into item elements. Other +containers may appear as their standard formatted text unless iteration is +requested. `no_iter` chooses text for an automatically handled range; `iter` +chooses XML elements and can name each item and its wrapper. + +These two representations need different readers. Iterated XML reconstructs +items from elements. Text representations use `from_string`, including its +container and tuple parsers. Borrowed views do not own storage, so they have no +built-in reconstruction. + +A custom `format` annotation changes a textual representation. It does not create +an inverse conversion: a custom prefix or hexadecimal format needs a matching +`from_string` specialization, and rounded-away precision cannot be restored. +Use the [annotation reference](annotations.md) and [XML input reference](xml-input.md) +for the precise rules and precedence. + +## Missing is different from empty + +A missing field can mean an incomplete document, an omitted optional value, or an +intentional choice to retain a default. SerialXML therefore distinguishes required +members, `std::optional`, and the `optional` annotation. + +Required members enforce the expected schema. An absent `std::optional` resets to +`nullopt`. An annotated optional member preserves its initializer or the existing +value in an in-place update. An empty element is still present, so it is suitable +for an empty string but cannot be interpreted as a number. + +## External schemas adapt existing classes + +A mock schema declares the accessible field and getter identifiers to expose, +plus their annotations and output order. It is never constructed; values come +from the real object. This is useful when you cannot modify the original type or +want a particular XML representation for it. + +The schema's annotations replace those on the selected target members. Nested +objects use their own annotations. Mapping uses directly accessible members; +inherited members are not included. Compile-time validation catches missing, +ambiguous, or incompatible mappings before a document is written. + +## Formatting a document can change its whitespace + +Whitespace can be data, particularly with direct text and CDATA. See +[whitespace and round trips](explanation/round-trips.md#formatting-a-document-can-change-its-whitespace) +for the tradeoff between readable output and faithful reconstruction. + +## Reading into an object is an update + +Conversion and setter failures can leave earlier members updated. See +[update behavior](explanation/round-trips.md#reading-into-an-object-is-an-update) +for the implications and the temporary-object approach. + +SerialXML validates its XML subset and matches namespace prefixes literally; +it does not resolve XML namespaces or process DTDs. It is a type-to-document +mapper rather than an XML schema validator. See the [API reference](api.rst) for +entry points and the [read guide](deserialization.md) for practical usage. diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..3c73cf3 --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,92 @@ + + +# Example programs + +Build and run the example applications included with SerialXML. Each example +imports the library module and writes XML from ordinary C++ records or annotated +schemas. Use this guide to run the repository programs without creating a separate +consumer application. + +## Run the accompanying examples + +From the repository root, configure a release build with examples enabled and +benchmarks disabled: + +```bash +cmake --preset release-gcc -DBUILD_EXAMPLES=ON -DBUILD_BENCHMARKS=OFF +cmake --build --preset build-release +``` + +The dev container supplies the required GCC, CMake, and Ninja toolchain. Keeping +benchmarks disabled lets you build the examples without Boost Serialization or +cereal. The executables are written under `build/release/examples/`. + +## Inspect the first program + +The first portion of `examples/hello_world.cpp` imports the modules and defines +the record: + +```{literalinclude} ../examples/hello_world.cpp +:language: cpp +:lines: 1-7 +``` + +The remaining portion creates a record, serializes it, and prints readable XML: + +```{literalinclude} ../examples/hello_world.cpp +:language: cpp +:lines: 9- +``` + +This is the complete source of the first example. Its default `to_xml` call +includes the XML declaration. + +## Run and check the output + +```bash +./build/release/examples/hello_world +``` + +The application prints: + +```xml + + + 3 + pizza + +``` + +Run another program by using its source filename without `.cpp`. For example: + +```bash +./build/release/examples/attributes +./build/release/examples/deserialization +``` + +The attribute program shows scalar values on opening tags. The deserialization +program demonstrates a custom product-code conversion, a selected setter, and +an in-place update. Some programs print labels before XML; those labels are not +part of the documents. + +## Choose an example + +The task guides below explain the code in portions and include the resulting +output. The repository programs may exercise additional variations of each task. + +| Task guide | +| --- | +| [Serialize a simple record](how-to/simple-record.md) | +| [Put a value in an XML attribute](how-to/attributes.md) | +| [Match an existing XML naming scheme](how-to/naming.md) | +| [Leave a field out of the schema](how-to/skip-fields.md) | +| [Serialize a nested object](how-to/nested-objects.md) | +| [Annotate a class you cannot modify](how-to/external-schema.md) | +| [Round-trip a custom leaf representation](how-to/custom-conversion.md) | +| [Choose a container representation](how-to/containers.md) | +| [Choose item and container names](how-to/item-names.md) | +| [Omit a value that is not present](how-to/optional-values.md) | +| [Write direct text or CDATA](how-to/text-and-cdata.md) | +| [Format a numeric value](how-to/numeric-formatting.md) | +| [Preserve XML-special characters](how-to/escaping.md) | +| [Combine nesting and annotations](how-to/combined-schema.md) | diff --git a/docs/explanation/index.md b/docs/explanation/index.md new file mode 100644 index 0000000..92c7eea --- /dev/null +++ b/docs/explanation/index.md @@ -0,0 +1,25 @@ +# Explanation + +Understand why SerialXML maps a type to a particular document and what makes a +round trip reliable. These pages discuss the model and its tradeoffs; use the +[how-to guides](../how-to/index.md) when you need implementation steps. + +## The mapping model + +[How SerialXML maps C++ to XML](../design.md) connects reflection, annotations, +external schemas, and conversion. Read it to understand how your C++ declarations +determine the document's structure. + +## Round-trip tradeoffs + +[Designing reliable round trips](round-trips.md) discusses optionality, textual +formatting, ownership, whitespace, and update failures. Read it when choosing a +schema that needs to be both written and read. + +```{toctree} +:hidden: +:maxdepth: 1 + +../design +round-trips +``` diff --git a/docs/explanation/round-trips.md b/docs/explanation/round-trips.md new file mode 100644 index 0000000..514cff0 --- /dev/null +++ b/docs/explanation/round-trips.md @@ -0,0 +1,73 @@ +# Designing reliable round trips + +A document that can be written is not always enough to reconstruct the original +object. A reliable round trip needs both a structural mapping and a reversible +text representation. The [mapping model](../design.md) explains the structure; +this page discusses the choices that can lose information or change state. + +## Text needs a reversible conversion + +A `format` annotation determines output text. The reader uses `from_string` and +cannot infer the inverse of an arbitrary formatter. Zero-padded decimal numbers +remain readable by the default numeric parser; a custom prefix or hexadecimal +representation needs a matching conversion. Rounded precision is already lost +before XML is written. + +The same distinction applies to containers: repeated XML items encode structure, +while a formatted container is one leaf value. The representation determines +which input conversion runs. + +## Absence expresses a policy + +A required field rejects incomplete input. An absent `std::optional` resets to +`nullopt`. An `optional` annotation preserves an initializer or the current value. +These policies answer different questions: whether a document must supply a +value, whether a value exists, and whether an update should retain old state. +An empty element is still present; it can represent an empty string but cannot +supply a valid number. + +## Reconstructed values need storage + +Owning strings and containers can retain decoded input. Borrowed views such as +`std::string_view` and `std::span` do not allocate storage, so they have no built-in +reconstruction. A custom conversion must give its result an appropriate lifetime. +The in-place reader's input snapshot protects parsing from field updates; it does +not give borrowed outputs permanent storage. + +## Formatting a document can change its whitespace + +Pretty printing adds indentation between elements. Text-bearing subtrees retain +their bytes because whitespace can be part of a string or mixed content. CDATA +and `xml:space` also require preservation. + +For raw scalar fields beside child elements, the parent's direct text is data. +Added indentation can become part of that data when read back. Keep compact +`to_xml` output as your stored representation when whitespace matters, and use +`prettify` for display. + +CDATA is another representation with boundaries that can be ambiguous between +adjacent fields. Tagged string members provide explicit boundaries and are easier +to reconstruct reliably. The [XML input reference](../xml-input.md) describes the +canonical split behavior and supported XML subset. + +## Reading into an object is an update + +The value-returning `from_xml` initializes an object and fills its members. +The in-place overload updates an existing object, which also allows types without +a default constructor. It snapshots the input before parsing so changing a field +that owns the input cannot invalidate the parser's text. + +Parsing completes before assignments begin. Conversion and setter failures can +still happen during assignment, leaving earlier members updated. When application +logic needs all-or-nothing replacement, deserialize into a separate value and +replace the destination after successful conversion. + +## Choose the contract before the representation + +Tagged strings provide explicit boundaries. Compact XML preserves mixed-content +whitespace. A reversible leaf format preserves values. A separate temporary +object allows replacement only after a successful read. These choices follow +from the data your application needs to retain. + +See the [XML input reference](../xml-input.md) for exact guarantees, or +[read XML into objects](../deserialization.md) to apply these choices. diff --git a/docs/how-to/attributes.md b/docs/how-to/attributes.md new file mode 100644 index 0000000..8343eb4 --- /dev/null +++ b/docs/how-to/attributes.md @@ -0,0 +1,52 @@ +(example-attributes)= +# Put a value in an XML attribute + +Use an attribute for scalar metadata while keeping other values as child elements. + +Use a CMake application linked to `serial_xml::serial_xml`; see +[installation](../installation.md) if it is not configured yet. Save the following +portions in `main.cpp`, in the order shown. + +## Choose the attribute member + +```{literalinclude} ../_snippets/how-to/attributes.cpp +:language: cpp +:lines: 1-7 +``` + +The annotation moves `x` to the opening tag. `y` stays a child element. Both members are still ordinary integers in your C++ object. + +## Write and read the XML + +Add this complete `main` function below the declarations: + +```{literalinclude} ../_snippets/how-to/attributes.cpp +:language: cpp +:lines: 9- +``` + +The reader looks for `x` as an attribute and `y` as a child. Their placement comes from the same declaration used by the writer. Attribute order in the input does not need to match C++ declaration order. + +## Build and check the result + +From your application directory, run: + +```bash +cmake -S . -B build -G Ninja +cmake --build build +./build/my_app +``` + +The program prints: + +```xml +5 +``` + +```text +x: 4; y: 5 +``` + +## Apply this to your schema + +Use `attribute` on scalar values. A nested object needs child structure; do not combine an attribute with unpacking, `raw`, or `cdata`. diff --git a/docs/how-to/combined-schema.md b/docs/how-to/combined-schema.md new file mode 100644 index 0000000..92fcadc --- /dev/null +++ b/docs/how-to/combined-schema.md @@ -0,0 +1,67 @@ +(example-advanced-mixed)= +# Combine nesting and annotations + +Build a department document with employee attributes, nested addresses, and named skill items. + +Use a CMake application linked to `serial_xml::serial_xml`; see +[installation](../installation.md) if it is not configured yet. Save the following +portions in `main.cpp`, in the order shown. + +## Define the schema at each level + +```{literalinclude} ../_snippets/how-to/combined-schema.cpp +:language: cpp +:lines: 1-20 +``` + +`Address` renames a child. `Employee` chooses an attribute name and names its skill items. `Department` names its attribute and the employees wrapper. Declare inner records first so each type is available to the next. + +## Write and read the XML + +Add this complete `main` function below the declarations: + +```{literalinclude} ../_snippets/how-to/combined-schema.cpp +:language: cpp +:lines: 22- +``` + +The `team` wrapper contains an `element` for each employee because that vector has no custom item name. Inside each employee, the address and skill annotations apply at their own nesting levels. + +## Build and check the result + +From your application directory, run: + +```bash +cmake -S . -B build -G Ninja +cmake --build build +./build/my_app +``` + +The program prints: + +```xml + + + + Alice +
+ 123 Main St + Boston + 2128 +
+ + C++ + Python + +
+
+
+``` + +```text +Department: Engineering; employee: Alice; city: Boston; skills: 2 +``` + +## Apply this to your schema + +To name each employee item `employee`, replace the employees member’s `name` annotation with `iter{"employee", "team"}`. Keep annotations on the member or type whose part of the XML you want to control. diff --git a/docs/how-to/containers.md b/docs/how-to/containers.md new file mode 100644 index 0000000..4c6d6dd --- /dev/null +++ b/docs/how-to/containers.md @@ -0,0 +1,55 @@ +(example-stl-containers)= +# Choose a container representation + +Choose repeated elements for structured XML, formatted text for a leaf value, or omission for an empty sequence. + +Use a CMake application linked to `serial_xml::serial_xml`; see +[installation](../installation.md) if it is not configured yet. Save the following +portions in `main.cpp`, in the order shown. + +## Define the three representations + +```{literalinclude} ../_snippets/how-to/containers.cpp +:language: cpp +:lines: 1-14 +``` + +A plain vector iterates automatically. `exclude_on_empty` suppresses its wrapper when empty. `no_iter` turns the entire vector into one formatted text value. + +## Write and read the XML + +Add this complete `main` function below the declarations: + +```{literalinclude} ../_snippets/how-to/containers.cpp +:language: cpp +:lines: 16- +``` + +The first document contains three `element` children. The second retains an empty wrapper; the third omits it. The reader reconstructs repeated items from elements and the formatted representation through `from_string`. + +## Build and check the result + +From your application directory, run: + +```bash +cmake -S . -B build -G Ninja +cmake --build build +./build/my_app +``` + +The program prints: + +```xml +123 + + +[1, 2, 3] +``` + +```text +Items: 3; text items: 3; omitted items: 0 +``` + +## Apply this to your schema + +An absent automatically iterated range marked `exclude_on_empty` becomes empty. Add `optional` if an absent input field should retain an initializer or existing value instead. Use owning containers when the reader needs to reconstruct storage. diff --git a/docs/how-to/custom-conversion.md b/docs/how-to/custom-conversion.md new file mode 100644 index 0000000..dfc31d8 --- /dev/null +++ b/docs/how-to/custom-conversion.md @@ -0,0 +1,76 @@ +(example-deserialization)= +# Round-trip a custom leaf representation + +Use a formatter and a matching input conversion when XML must represent a value +as custom text. This example writes a product code as `SKU-42` and reads it back +as an integer-bearing type. + +Use a CMake application linked to `serial_xml::serial_xml`; see +[installation](../installation.md) if needed. Put the following portions in +`main.cpp` in order. + +## Define the leaf and its output format + +```{literalinclude} ../_snippets/how-to/custom-conversion.cpp +:language: cpp +:lines: 1-8 +``` + +`ProductCode` holds the value. `format_code` provides its textual representation +and returns an owning string. The prefix is part of the XML format, not the stored +integer. + +## Supply the inverse conversion + +```{literalinclude} ../_snippets/how-to/custom-conversion.cpp +:language: cpp +:lines: 10-16 +``` + +Declare the specialization before the first use that needs it. It checks the +prefix, then delegates numeric conversion to SerialXML. That conversion rejects +invalid numeric text and overflow. The output formatter alone cannot tell the +reader how to remove the prefix. + +## Attach the formatter to the record + +```{literalinclude} ../_snippets/how-to/custom-conversion.cpp +:language: cpp +:lines: 18-21 +``` + +The `format` annotation makes `code` a text leaf. The `id` member remains an +attribute. Reading a `Product` passes the leaf’s decoded text to the specialization. + +## Write, read, and handle invalid input + +```{literalinclude} ../_snippets/how-to/custom-conversion.cpp +:language: cpp +:lines: 23- +``` + +The successful round trip restores the underlying integer. The final call checks +that the custom parser reports a representation with the wrong prefix. + +## Build and check the result + +```bash +cmake -S . -B build -G Ninja +cmake --build build +./build/my_app +``` + +The program prints: + +```xml +SKU-42 +``` + +```text +Product: 7; code: 42 +Conversion failed: Expected SKU- code +``` + +Keep output and input formats compatible when changing the prefix or numeric +representation. A lossy formatter, such as one that rounds away precision, cannot +be inverted to recover the original value. diff --git a/docs/how-to/escaping.md b/docs/how-to/escaping.md new file mode 100644 index 0000000..bb513c4 --- /dev/null +++ b/docs/how-to/escaping.md @@ -0,0 +1,54 @@ +(example-escaping)= +# Preserve XML-special characters + +Pass ordinary strings to SerialXML and let it escape the characters that have meaning in XML. + +Use a CMake application linked to `serial_xml::serial_xml`; see +[installation](../installation.md) if it is not configured yet. Save the following +portions in `main.cpp`, in the order shown. + +## Define the three text contexts + +```{literalinclude} ../_snippets/how-to/escaping.cpp +:language: cpp +:lines: 1-14 +``` + +The plain member becomes a child, the attribute belongs on the opening tag, and the raw member contributes direct text. Each representation still requires valid XML text. + +## Write and read the XML + +Add this complete `main` function below the declarations: + +```{literalinclude} ../_snippets/how-to/escaping.cpp +:language: cpp +:lines: 16- +``` + +The original string contains all five XML-special characters. Each output escapes them, and the reader decodes the child document back into the original string. + +## Build and check the result + +From your application directory, run: + +```bash +cmake -S . -B build -G Ninja +cmake --build build +./build/my_app +``` + +The program prints: + +```xml +<>&'" + +<>&'" +``` + +```text +Restored text: <>&'" +``` + +## Apply this to your schema + +Do not pre-escape strings: an existing ampersand in `&` is itself escaped. `raw` removes enclosing tags, not escaping. Malformed UTF-8 and invalid XML 1.0 characters cause serialization to throw `std::invalid_argument`. diff --git a/docs/how-to/external-schema.md b/docs/how-to/external-schema.md new file mode 100644 index 0000000..608a5c6 --- /dev/null +++ b/docs/how-to/external-schema.md @@ -0,0 +1,52 @@ +(example-mocking)= +# Annotate a class you cannot modify + +Use an external schema to expose a const getter on an existing class. This example reads the size of a standard vector without modifying its definition. + +Use a CMake application linked to `serial_xml::serial_xml`; see +[installation](../installation.md) if it is not configured yet. Save the following +portions in `main.cpp`, in the order shown. + +## Declare the external schema + +```{literalinclude} ../_snippets/how-to/external-schema.cpp +:language: cpp +:lines: 1-7 +``` + +The placeholder getter has the same identifier as the vector’s accessible `size()` method. It needs no definition: the library calls the method on the actual vector, and never constructs a mock. + +## Write the selected representation + +Add this complete `main` function below the declarations: + +```{literalinclude} ../_snippets/how-to/external-schema.cpp +:language: cpp +:lines: 9- +``` + +The template argument selects the mock’s annotations. The attribute contains the actual size, while the root uses the real type’s identifier, `vector`. The schema selects only `size`, so the vector’s elements are not written. + +## Build and check the result + +From your application directory, run: + +```bash +cmake -S . -B build -G Ninja +cmake --build build +./build/my_app +``` + +The program prints: + +```xml + +``` + +```text +Actual vector size: 3 +``` + +## Apply this to your schema + +This schema cannot reconstruct the vector’s contents: a size getter has no writable counterpart, and the document has no items. For an input schema, expose writable fields or setters. Match directly accessible member identifiers and field/getter kinds; invalid or ambiguous mappings are diagnosed at compile time. diff --git a/docs/how-to/index.md b/docs/how-to/index.md new file mode 100644 index 0000000..14366d7 --- /dev/null +++ b/docs/how-to/index.md @@ -0,0 +1,67 @@ +# How-to guides + +Choose the result you need. These guides assume you understand C++ types and have +completed the [first round trip](../quickstart.md) or already use SerialXML. +Each recipe presents a complete program in portions, explains the schema choices, +and includes build commands and expected output. + +## Set up and run + +- [Install SerialXML in a CMake project](../installation.md). +- [Run the example programs](../examples.md#run-the-accompanying-examples). +- [Run the benchmarks](../benchmarking.md). + +## Write and shape XML + +- [Write a document, fragment, or named root](../serialization.md). +- [Serialize a simple record](simple-record.md). +- [Put a value in an attribute](attributes.md). +- [Match an XML naming scheme](naming.md). +- [Leave a field out of the schema](skip-fields.md). +- [Serialize nested objects](nested-objects.md). +- [Adapt a class with an external schema](external-schema.md). +- [Combine nesting and annotations](combined-schema.md). + +## Represent collections and text + +- [Choose a container representation](containers.md). +- [Choose item and container names](item-names.md). +- [Omit an absent optional value](optional-values.md). +- [Write direct text or CDATA](text-and-cdata.md). +- [Format a numeric value](numeric-formatting.md). +- [Preserve XML-special characters](escaping.md). + +## Read and update objects + +- [Read XML into a new or existing object](../deserialization.md). +- [Retain defaults for missing fields](../deserialization.md#retain-defaults-when-a-field-is-missing). +- [Round-trip a custom leaf representation](custom-conversion.md). +- [Read private state through setters](../deserialization.md#read-through-setters). +- [Diagnose a read failure](../deserialization.md#diagnose-a-read-failure). + +Need an exact rule rather than a procedure? Open the [reference](../reference/index.md). + +```{toctree} +:hidden: +:maxdepth: 1 + +../installation +../serialization +simple-record +attributes +naming +skip-fields +nested-objects +external-schema +combined-schema +containers +item-names +optional-values +text-and-cdata +numeric-formatting +escaping +../deserialization +custom-conversion +../examples +../benchmarking +``` diff --git a/docs/how-to/item-names.md b/docs/how-to/item-names.md new file mode 100644 index 0000000..8f73a41 --- /dev/null +++ b/docs/how-to/item-names.md @@ -0,0 +1,60 @@ +(example-iteration)= +# Choose item and container names + +Replace the default range wrapper and `element` item names with the names required by your schema. + +Use a CMake application linked to `serial_xml::serial_xml`; see +[installation](../installation.md) if it is not configured yet. Save the following +portions in `main.cpp`, in the order shown. + +## Specify the item and wrapper names + +```{literalinclude} ../_snippets/how-to/item-names.cpp +:language: cpp +:lines: 1-6 +``` + +The first `iter` argument names each item; the second names their enclosing element. The C++ member remains `values`. + +## Write and read the XML + +Add this complete `main` function below the declarations: + +```{literalinclude} ../_snippets/how-to/item-names.cpp +:language: cpp +:lines: 8- +``` + +The writer puts each number inside a `number` element under `numbers`. The reader expects those same names and fills the vector in item order. + +## Build and check the result + +From your application directory, run: + +```bash +cmake -S . -B build -G Ninja +cmake --build build +./build/my_app +``` + +The program prints: + +```xml + + + 1 + 2 + 3 + + +``` + +```text +Number: 1 +Number: 2 +Number: 3 +``` + +## Apply this to your schema + +Use `iter{"number"}` to change only the item name and retain the member’s wrapper name. For a range of records, each named item contains that record’s reflected members. diff --git a/docs/how-to/naming.md b/docs/how-to/naming.md new file mode 100644 index 0000000..7e1d2f9 --- /dev/null +++ b/docs/how-to/naming.md @@ -0,0 +1,52 @@ +(example-named-tags)= +# Match an existing XML naming scheme + +Use explicit names when an XML schema uses different identifiers from your C++ code. + +Use a CMake application linked to `serial_xml::serial_xml`; see +[installation](../installation.md) if it is not configured yet. Save the following +portions in `main.cpp`, in the order shown. + +## Name the root and child + +```{literalinclude} ../_snippets/how-to/naming.cpp +:language: cpp +:lines: 1-7 +``` + +Place the root annotation after the `struct` keyword. The member annotation changes `favorite_food` to `food` in XML without changing its C++ identifier. + +## Write and read the XML + +Add this complete `main` function below the declarations: + +```{literalinclude} ../_snippets/how-to/naming.cpp +:language: cpp +:lines: 9- +``` + +Both writing and reading use `person` as the root, `age` as an attribute, and `food` as a child. An annotation on a nested type applies within its containing member wrapper. + +## Build and check the result + +From your application directory, run: + +```bash +cmake -S . -B build -G Ninja +cmake --build build +./build/my_app +``` + +The program prints: + +```xml +pizza +``` + +```text +Age: 3; food: pizza +``` + +## Apply this to your schema + +To override only the root at runtime, pass the same name to both calls: `to_xml(original, false, "customer")` and `from_xml(xml, "customer")`. That override takes precedence over the root annotation. diff --git a/docs/how-to/nested-objects.md b/docs/how-to/nested-objects.md new file mode 100644 index 0000000..458109c --- /dev/null +++ b/docs/how-to/nested-objects.md @@ -0,0 +1,59 @@ +(example-nested-structs)= +# Serialize a nested object + +Write a record containing another record without flattening the nested fields. + +Use a CMake application linked to `serial_xml::serial_xml`; see +[installation](../installation.md) if it is not configured yet. Save the following +portions in `main.cpp`, in the order shown. + +## Define the inner and outer records + +```{literalinclude} ../_snippets/how-to/nested-objects.cpp +:language: cpp +:lines: 1-13 +``` + +Declare `Address` before `Person`, then use it as the member type. These records have accessible data members and no custom textual formatter, so reflection expands their fields. + +## Write and read the XML + +Add this complete `main` function below the declarations: + +```{literalinclude} ../_snippets/how-to/nested-objects.cpp +:language: cpp +:lines: 15- +``` + +The nested wrapper is named `address`, after the member, rather than `Address`, after the type. Its children come from the inner record. Reading reconstructs both levels from the same structure. + +## Build and check the result + +From your application directory, run: + +```bash +cmake -S . -B build -G Ninja +cmake --build build +./build/my_app +``` + +The program prints: + +```xml + + Alice +
+ 123 Main St + Boston + 2128 +
+
+``` + +```text +Alice lives in Boston (2128) +``` + +## Apply this to your schema + +An attribute annotation on an `Address` member places that value on the `address` wrapper. Add annotations at the level whose XML shape you want to change. diff --git a/docs/how-to/numeric-formatting.md b/docs/how-to/numeric-formatting.md new file mode 100644 index 0000000..49c3ef5 --- /dev/null +++ b/docs/how-to/numeric-formatting.md @@ -0,0 +1,52 @@ +(example-format-specifiers)= +# Format a numeric value + +Use a format annotation to give a numeric field a fixed decimal width. + +Use a CMake application linked to `serial_xml::serial_xml`; see +[installation](../installation.md) if it is not configured yet. Save the following +portions in `main.cpp`, in the order shown. + +## Attach the decimal format + +```{literalinclude} ../_snippets/how-to/numeric-formatting.cpp +:language: cpp +:lines: 1-6 +``` + +`03d` requests three decimal digits with leading zeroes. Pass the specification without a leading colon or surrounding braces. Combine it with `attribute` to format the value on the opening tag. + +## Write and read the XML + +Add this complete `main` function below the declarations: + +```{literalinclude} ../_snippets/how-to/numeric-formatting.cpp +:language: cpp +:lines: 8- +``` + +The writer emits `042`. The default numeric reader accepts the zero-padded decimal representation and restores the integer 42. + +## Build and check the result + +From your application directory, run: + +```bash +cmake -S . -B build -G Ninja +cmake --build build +./build/my_app +``` + +The program prints: + +```xml + +``` + +```text +Restored value: 42 +``` + +## Apply this to your schema + +The same format works on a child member without `attribute`. Hexadecimal text, custom prefixes, and alignment fill need a matching input conversion. A format that rounds away precision cannot restore the original value. diff --git a/docs/how-to/optional-values.md b/docs/how-to/optional-values.md new file mode 100644 index 0000000..559e5cc --- /dev/null +++ b/docs/how-to/optional-values.md @@ -0,0 +1,54 @@ +(example-optional-types)= +# Omit a value that is not present + +Use an owning `std::optional` when a field may have no value in your XML document. + +Use a CMake application linked to `serial_xml::serial_xml`; see +[installation](../installation.md) if it is not configured yet. Save the following +portions in `main.cpp`, in the order shown. + +## Declare an optional field + +```{literalinclude} ../_snippets/how-to/optional-values.cpp +:language: cpp +:lines: 1-6 +``` + +An engaged optional is written as its contained integer. `nullopt` contributes no element. This also works with an optional member annotated as an attribute. + +## Write and read the XML + +Add this complete `main` function below the declarations: + +```{literalinclude} ../_snippets/how-to/optional-values.cpp +:language: cpp +:lines: 8- +``` + +The example first reads 42, then updates the same object from the document without a value. The reader resets the absent `std::optional` to `nullopt`. + +## Build and check the result + +From your application directory, run: + +```bash +cmake -S . -B build -G Ninja +cmake --build build +./build/my_app +``` + +The program prints: + +```xml +42 + +``` + +```text +Present value: 42 +After absent input: false +``` + +## Apply this to your schema + +To retain a current value when input omits the field, add `[[= serial_xml::optional]]`. That annotation changes the missing-input policy; it does not change what the writer emits. An empty `` is present and cannot be converted to an integer. diff --git a/docs/how-to/simple-record.md b/docs/how-to/simple-record.md new file mode 100644 index 0000000..7335ce5 --- /dev/null +++ b/docs/how-to/simple-record.md @@ -0,0 +1,105 @@ +(example-hello-world)= +# Serialize a simple record + +Use this guide when your C++ member names already match the XML you want to write. +The program below saves a `Person`, prints its XML, and reads the values back. + +Start with a CMake application linked to `serial_xml::serial_xml`. If you have +not set that up yet, follow [installation](../installation.md). Save the following +code in `main.cpp`. + +## Define the record + +Import the standard library and SerialXML, then declare the data you want to save: + +```cpp +import std; +import serial_xml; + +struct Person { + int age; + std::string favorite_food; +}; +``` + +No annotations are needed. `Person` becomes the root element, and `age` and +`favorite_food` become child elements in their declaration order. + +## Write the XML + +Add `main` below the record definition: + +```cpp +int main() { + const Person original{3, "pizza"}; + const std::string xml = serial_xml::to_xml(original, false); + std::println("{}", serial_xml::prettify(xml)); +} +``` + +`to_xml` returns the document as a string. The `false` argument omits the XML +declaration, which is useful when you want a fragment. Omit that argument when +you want the declaration included. + +Configure, build, and run the application from the directory containing your +`CMakeLists.txt`: + +```bash +cmake -S . -B build -G Ninja +cmake --build build +./build/my_app +``` + +The program prints: + +```xml + + 3 + pizza + +``` + +`prettify` adds indentation for display. The original `xml` string remains compact: + +```xml +3pizza +``` + +## Read the values back + +Replace `main` with this version to reconstruct the record from the same XML: + +```cpp +int main() { + const Person original{3, "pizza"}; + const std::string xml = serial_xml::to_xml(original, false); + std::println("{}", serial_xml::prettify(xml)); + + const Person restored = serial_xml::from_xml(xml); + std::println("Age: {}", restored.age); + std::println("Favorite food: {}", restored.favorite_food); +} +``` + +The `Person` template argument tells the reader which type to construct. It +matches the `Person` root and populates the members from their named child +elements. Pass the compact `xml` string directly; pretty printing is only needed +for the displayed document. + +After building and running again, the complete output is: + +```xml + + 3 + pizza + +``` + +```text +Age: 3 +Favorite food: pizza +``` + +Your `main.cpp` now consists of the imports and record definition from the first +section followed by this final `main` function. Change the initializer to +`Person{7, "pasta"}` to write and read different values using the same XML shape. diff --git a/docs/how-to/skip-fields.md b/docs/how-to/skip-fields.md new file mode 100644 index 0000000..4878396 --- /dev/null +++ b/docs/how-to/skip-fields.md @@ -0,0 +1,53 @@ +(example-skip-members)= +# Leave a field out of the schema + +Exclude internal state from XML while continuing to store it in your C++ object. + +Use a CMake application linked to `serial_xml::serial_xml`; see +[installation](../installation.md) if it is not configured yet. Save the following +portions in `main.cpp`, in the order shown. + +## Mark the internal member + +```{literalinclude} ../_snippets/how-to/skip-fields.cpp +:language: cpp +:lines: 1-7 +``` + +`skip` excludes `cached_value` from both writing and reading. Its initializer gives a newly constructed record a defined cache value. + +## Write and read the XML + +Add this complete `main` function below the declarations: + +```{literalinclude} ../_snippets/how-to/skip-fields.cpp +:language: cpp +:lines: 9- +``` + +Although `original.cached_value` is 100, it contributes no XML. A new object retains the initializer, while reading into an existing object retains that object’s current cache. + +## Build and check the result + +From your application directory, run: + +```bash +cmake -S . -B build -G Ninja +cmake --build build +./build/my_app +``` + +The program prints: + +```xml +42 +``` + +```text +New object: value 42; cache -1 +Updated object: cache 250 +``` + +## Apply this to your schema + +Use `skip` when the member is outside the XML schema. It is different from allowing an input field to be missing: an optional field still participates when present. diff --git a/docs/how-to/text-and-cdata.md b/docs/how-to/text-and-cdata.md new file mode 100644 index 0000000..80238ff --- /dev/null +++ b/docs/how-to/text-and-cdata.md @@ -0,0 +1,54 @@ +(example-raw-cdata)= +# Write direct text or CDATA + +Place string content directly inside its parent element instead of giving the member a child wrapper. + +Use a CMake application linked to `serial_xml::serial_xml`; see +[installation](../installation.md) if it is not configured yet. Save the following +portions in `main.cpp`, in the order shown. + +## Choose how direct text is represented + +```{literalinclude} ../_snippets/how-to/text-and-cdata.cpp +:language: cpp +:lines: 1-10 +``` + +`raw` removes the member’s wrapper but still escapes XML characters. `cdata` writes a CDATA section directly into the parent. Neither requires callers to prepare escaped input. + +## Write and read the XML + +Add this complete `main` function below the declarations: + +```{literalinclude} ../_snippets/how-to/text-and-cdata.cpp +:language: cpp +:lines: 12- +``` + +The raw document contains entity references where needed. The CDATA document keeps markup characters inside the section. Reading either representation reconstructs the original string. + +## Build and check the result + +From your application directory, run: + +```bash +cmake -S . -B build -G Ninja +cmake --build build +./build/my_app +``` + +The program prints: + +```xml +Hello <world> & friends + & stuff]]> +``` + +```text +Raw value: Hello & friends +CDATA value: text & stuff +``` + +## Apply this to your schema + +Use compact XML for mixed-content round trips: indentation can become part of direct text. Multiple raw scalar fields cannot be separated on input, and CDATA fields are read in member order. Prefer tagged strings when field boundaries need to be explicit. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..dd3be03 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,73 @@ +# SerialXML + +```{image} ../assets/SerialXML.png +:alt: SerialXML — Modern C++ XML Serialization, with C++26 reflection and native modules +:class: serialxml-banner +``` + +
C++26 · XML · Reflection
+ +## Your types. Your XML. + +Write ordinary C++ objects as XML and read them back. Start with reflection's +default mapping, then use annotations to match the schema your application needs. + + + +```cpp +import std; +import serial_xml; + +struct Person { + [[= serial_xml::attribute]] int age; + std::string name; +}; + +const auto xml = serial_xml::to_xml(Person{21, "Alex"}, false); +// Alex +const auto person = serial_xml::from_xml(xml); +``` + +## What do you want to do? + + + +## Common destinations + +| I need to… | Go to | +| --- | --- | +| Add SerialXML to my CMake project | [Installation](installation.md) | +| Rename a tag or make it an attribute | [XML naming](how-to/naming.md) · [Attributes](how-to/attributes.md) | +| Serialize a class I cannot change | [External schemas](how-to/external-schema.md) | +| Keep defaults when input fields are missing | [Read XML](deserialization.md#retain-defaults-when-a-field-is-missing) | +| Understand why reading XML fails | [Diagnose a read failure](deserialization.md#diagnose-a-read-failure) | +| Find a function or annotation | [API reference](api.rst) · [Annotation reference](annotations.md) | + +```{note} +**Toolchain:** GCC 16.1+ with C++26 reflection, CMake 4.3.3+, and Ninja. +The repository's dev container supplies a matching environment. +See [installation](installation.md) before building your application. +``` + +```{toctree} +:hidden: +:maxdepth: 2 + +tutorials/index +how-to/index +reference/index +explanation/index +project/index +``` + +[Source code](https://github.com/EJainDev/SerialXML) · +[Report an issue](https://github.com/EJainDev/SerialXML/issues) · +[Contribute](contributing.md) diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..f59d7cb --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,109 @@ +# Install SerialXML + +## Requirements + +| Component | Minimum version | Notes | +| --- | --- | --- | +| Compiler | GCC 16.1 | C++26 reflection, annotations, and SIMD | +| CMake | 4.3.3 | Enable experimental `import std` before `project()` | +| Build system | Ninja | Used by the repository presets | +| C++ standard | 26 | Required by the library | + +The dev container uses GCC 16.2.0 and CMake 4.4.3. Other compilers are not currently +supported. StructuralTuple is pinned to a tested commit. + +## Create a consumer application + +Create an empty application directory. Save this complete configuration as +`CMakeLists.txt`; it selects C++26, enables standard-library modules, fetches +SerialXML, and links the executable to its exported target. + +### Configure CMake FetchContent + +```{literalinclude} _snippets/CMakeLists.txt +:language: cmake +:caption: CMakeLists.txt +``` + +## Add the application code + +Save this program as `main.cpp` beside `CMakeLists.txt`: + +```cpp +import std; +import serial_xml; + +struct Record { + int value; +}; + +int main() { + const auto xml = serial_xml::to_xml(Record{42}, false); + std::println("{}", xml); + const auto restored = serial_xml::from_xml(xml); + std::println("Restored value: {}", restored.value); +} +``` + +You now have both files needed for the consumer. `import serial_xml` uses the +module built by CMake; linking the target supplies the reflection compiler options. + +## Install from source + +```bash +git clone https://github.com/EJainDev/SerialXML.git +cd SerialXML + +cmake --preset release-gcc -DBUILD_BENCHMARKS=OFF +cmake --build --preset build-release +cmake --install build/release --prefix "$HOME/.local" +``` + +Use the same compiler and standard-module setup shown above, then replace the +FetchContent block with: + +```cmake +find_package(SerialXML 1.0 CONFIG REQUIRED) +add_executable(my_app main.cpp) +target_link_libraries(my_app PRIVATE serial_xml::serial_xml) +``` + +Configure your application with `-DCMAKE_PREFIX_PATH="$HOME/.local"` for this +install prefix. A source install also installs the fetched StructuralTuple dependency; +if using an existing StructuralTuple package, make its prefix available to consumers too. +Reflection compiler options and the C++26 requirement propagate from the library target. +FetchContent builds default to disabling examples and benchmarks when embedded. + +## Configure and build the consumer + +Save the CMake configuration as `CMakeLists.txt` alongside your `main.cpp`. +With the required GCC selected as your C++ compiler, run: + +```bash +cmake -S . -B build -G Ninja +cmake --build build +./build/my_app +``` + +The consumer program above prints: + +```xml +42 +``` + +```text +Restored value: 42 +``` + +## Troubleshoot integration + +| Symptom | Check | +| --- | --- | +| Reflection syntax is rejected | Use the required GCC build; a `-std=c++26` flag alone does not supply reflection support. | +| CMake rejects the experimental `import std` key | Match the opt-in key to your CMake version, before `project()`. The configuration above covers the repository's supported versions. | +| `serial_xml` cannot be imported | Link the executable to `serial_xml::serial_xml` and let CMake build the module. | +| A package cannot be found after source installation | Set `CMAKE_PREFIX_PATH` to the installation prefix and make StructuralTuple available too. | +| Benchmarks fail while building the repository | Configure with `-DBUILD_BENCHMARKS=OFF` if Boost Serialization or cereal is unavailable. | + +After changing the compiler, configure a new build directory to avoid reusing +module artifacts from another toolchain. diff --git a/docs/license.md b/docs/license.md new file mode 100644 index 0000000..d2940db --- /dev/null +++ b/docs/license.md @@ -0,0 +1,6 @@ +# License + +SerialXML is distributed under the MIT License. + +See the repository's [LICENSE](https://github.com/EJainDev/SerialXML/blob/main/LICENSE) +for the complete terms and copyright notice. diff --git a/docs/project/index.md b/docs/project/index.md new file mode 100644 index 0000000..d851e3c --- /dev/null +++ b/docs/project/index.md @@ -0,0 +1,12 @@ +# Project + +Work on SerialXML itself, build this documentation, or check the license. +For application integration, start with [installation](../installation.md). + +```{toctree} +:maxdepth: 1 + +../contributing +../building-docs +../license +``` diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 0000000..7817658 --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,118 @@ +# Tutorial: your first XML round trip + +In this tutorial, you will build a small program that writes a `Person` to XML, +prints the document, and reads the same values back. You will use an ordinary +struct with no annotations. + +## 1. Prepare the application + +You need GCC 16.1+ with reflection support, CMake 4.3.3+, Ninja, and network +access for the CMake dependencies. The repository's dev container supplies the +toolchain. Select that GCC as your compiler before configuring the application. + +Create an empty directory for this tutorial. Save the following configuration as +`CMakeLists.txt` in that directory: + +```{literalinclude} _snippets/CMakeLists.txt +:language: cmake +:caption: CMakeLists.txt +``` + +Create `main.cpp` beside it. Import the standard library and SerialXML, then define the +object you will save: + +```cpp +import std; +import serial_xml; + +struct Person { + int age; + std::string favorite_food; +}; +``` + +## 2. Write the object to XML + +Add a `main` function that creates a person and serializes it: + +```cpp +int main() { + const Person original{3, "pizza"}; + const auto xml = serial_xml::to_xml(original); + std::println("{}", serial_xml::prettify(xml)); +} +``` + +Configure and build your application from its root: + +```bash +cmake -S . -B build -G Ninja +cmake --build build +./build/my_app +``` + +You should see: + +```xml + + + 3 + pizza + +``` + +The struct name becomes the root element; its members become child elements. +`to_xml` produces compact XML. `prettify` makes that document readable for display. + +## 3. Read the document back + +Before the closing brace of `main`, add: + +```cpp +const auto restored = serial_xml::from_xml(xml); +std::println("Age: {}; favorite food: {}", restored.age, restored.favorite_food); +``` + +Build and run again. The final line should be: + +```text +Age: 3; favorite food: pizza +``` + +You have now saved and restored the same object. The `Person` definition tells +SerialXML how to interpret the XML without a separate runtime schema. + +## 4. Change a value + +Change `original` to `Person{7, "pasta"}`, rebuild, and run. The XML should contain +`7` and `pasta`, and the final line should +show the new values. The shape of the document stays the same. + +## Completed program + +Your `main.cpp` should now look like this (using the original values): + +```cpp +import std; +import serial_xml; + +struct Person { + int age; + std::string favorite_food; +}; + +int main() { + const Person original{3, "pizza"}; + const auto xml = serial_xml::to_xml(original); + std::println("{}", serial_xml::prettify(xml)); + const auto restored = serial_xml::from_xml(xml); + std::println("Age: {}; favorite food: {}", restored.age, restored.favorite_food); +} +``` + +## Continue learning + +Continue with [Shape an XML document](tutorials/schema.md) to add attributes, +rename fields, and serialize a collection. If the application did not build, +check [integration troubleshooting](installation.md#troubleshoot-integration). +For a specific task, browse the [how-to guides](how-to/index.md). diff --git a/docs/reference/index.md b/docs/reference/index.md new file mode 100644 index 0000000..864c254 --- /dev/null +++ b/docs/reference/index.md @@ -0,0 +1,32 @@ +# Reference + +Use reference pages to check contracts, supported inputs, and exact behavior. +For instructions that change your code, use the [how-to guides](../how-to/index.md). + +| Look up | Page | +| --- | --- | +| Function signatures, parameters, return values, and exceptions | [API reference](../api.rst) | +| XML annotations and their precedence | [Annotation reference](../annotations.md) | +| XML syntax, missing values, conversions, and update guarantees | [XML input reference](../xml-input.md) | +| Recorded performance and measurement environment | [Benchmark results](../benchmarks.md) | +| An exported C++ symbol alphabetically | {ref}`genindex` | +| A term anywhere in the documentation | {ref}`search` | + +## Frequent lookups + +- [Missing and empty values](../xml-input.md#missing-and-empty-values). +- [Unknown elements, ordering, and duplicates](../xml-input.md#order-unknown-content-and-duplicates). +- [Text and attribute normalization](../xml-input.md#text-and-attribute-normalization). +- [Raw text and CDATA](../xml-input.md#raw-text-and-cdata). +- [Failure and update behavior](../xml-input.md#failure-and-update-behavior). +- [Automatic ranges and annotation precedence](../annotations.md#precedence-and-automatic-handling). + +```{toctree} +:hidden: +:maxdepth: 1 + +../api +../annotations +../xml-input +../benchmarks +``` diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..3ffb9b7 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,34 @@ +accessible-pygments==0.0.5 +alabaster==1.0.0 +babel==2.18.0 +beautifulsoup4==4.15.0 +certifi==2026.7.22 +charset-normalizer==3.5.2 +docutils==0.21.2 +furo==2025.12.19 +idna==3.20 +imagesize==2.0.1 +Jinja2==3.1.6 +markdown-it-py==3.0.0 +MarkupSafe==3.0.4 +mdit-py-plugins==0.6.1 +mdurl==0.1.2 +myst-parser==4.0.1 +packaging==26.3 +Pygments==2.21.0 +PyYAML==6.0.3 +requests==2.34.2 +roman-numerals==4.1.0 +roman-numerals-py==4.1.0 +snowballstemmer==3.1.1 +soupsieve==2.10 +Sphinx==8.2.3 +sphinx-basic-ng==1.0.0b2 +sphinxcontrib-applehelp==2.0.0 +sphinxcontrib-devhelp==2.0.0 +sphinxcontrib-htmlhelp==2.1.0 +sphinxcontrib-jsmath==1.0.1 +sphinxcontrib-qthelp==2.0.0 +sphinxcontrib-serializinghtml==2.0.0 +typing_extensions==4.16.0 +urllib3==2.8.0 diff --git a/docs/serialization.md b/docs/serialization.md new file mode 100644 index 0000000..5f3a435 --- /dev/null +++ b/docs/serialization.md @@ -0,0 +1,97 @@ +# Write XML and adapt schemas + +Choose whether to write a complete document, omit the declaration, or override the +root name for a particular destination. This application demonstrates all three +with the same record, then reads the named-root document back. + +Use a CMake application linked to `serial_xml::serial_xml`; see +[installation](installation.md) if needed. Place these portions in `main.cpp`. + +## Define the record + +```{literalinclude} _snippets/how-to/write-documents.cpp +:language: cpp +:lines: 1-6 +``` + +The type annotation gives the default root the name `record`. The member `value` +becomes a child with that name. + +## Write a document, a fragment, or a named root + +Add this complete application body below the declaration: + +```{literalinclude} _snippets/how-to/write-documents.cpp +:language: cpp +:lines: 8- +``` + +Calling `to_xml(record)` includes the UTF-8 XML declaration. Passing `false` +omits it. The third argument selects the root name at runtime and takes +precedence over the type’s `name` annotation. Pass that same root override to +`from_xml` when reading the document. + +## Format XML for display + +The `prettify(named)` call prints an indented version without changing `named`. +Keep the compact string for storage or parsing, especially if your schema has +raw text beside child elements. Text-bearing subtrees, CDATA, and subtrees with +`xml:space` retain their original bytes during pretty printing. + +## Build and run + +```bash +cmake -S . -B build -G Ninja +cmake --build build +./build/my_app +``` + +The output is: + +```text +Document: +``` + +```xml +42 +``` + +```text +Fragment: +``` + +```xml +42 +``` + +```text +Named root: +``` + +```xml +42 +``` + +```text +For display: +``` + +```xml + + 42 + +``` + +```text +Restored value: 42 +``` + +The root override must be a valid XML 1.0 name. An invalid override throws +`std::invalid_argument`; malformed input to `prettify` throws +`serial_xml::deserialization_error`. + +## Annotate a class you cannot modify + +For an existing type whose declarations you cannot edit, select an external schema +as the template argument to `to_xml`. The [external-schema guide](how-to/external-schema.md) +provides the complete declarations, application body, and output for that task. diff --git a/docs/tutorials/catalog.md b/docs/tutorials/catalog.md new file mode 100644 index 0000000..aa631fb --- /dev/null +++ b/docs/tutorials/catalog.md @@ -0,0 +1,103 @@ +# Build a book catalogue + +Create a document containing two books, then reconstruct the collection and print +its entries. You will learn how nested records and named range items combine in +one XML document. + +Use the application from [Shape an XML document](schema.md). Keep its +`CMakeLists.txt`; you will replace `main.cpp` for this project. You need the same +GCC reflection toolchain. Run the build commands from your application directory. + +## 1. Write one book + +Replace `main.cpp` with this program: + +```{literalinclude} ../_snippets/tutorials/catalog-1.cpp +:language: cpp +``` + +Build and run: + +```bash +cmake --build build +./build/my_app +``` + +You should see: + +```xml +The XML Handbook +``` + +The identifier is an attribute on the book; the title is a child. Keep the +imports and `Book` declaration for the next step. + +## 2. Put two books in a catalogue + +Add this declaration after `Book`, before `main`: + +```cpp +struct [[= serial_xml::name{"catalog"}]] Catalog { + [[= serial_xml::iter{"book"}]] std::vector books; +}; +``` + +The root annotation gives the document a lowercase name. The `iter` annotation +names each repeated item `book`; the enclosing member keeps its name `books`. +Replace `main` with: + +```{literalinclude} ../_snippets/tutorials/catalog-2.cpp +:language: cpp +:lines: 13- +``` + +Build and run again. This time the output is: + +```xml + + + + The XML Handbook + + + Reflection in C++ + + + +``` + +The `Book` annotations apply inside each `book` wrapper. There is no separate +`Book` element inside it. `prettify` makes the nesting visible for display. + +## 3. Read the collection back + +Replace `main` once more to read the compact document and print each entry: + +```{literalinclude} ../_snippets/tutorials/catalog.cpp +:language: cpp +:lines: 13- +``` + +Build and run. After the XML, you should see: + +```text +Restored books: 2 +1: The XML Handbook +2: Reflection in C++ +``` + +The reader reconstructs the vector in item order. Change the second title in the +initializer, rebuild, and confirm that both the XML and the restored entry show +your new title. + +## Completed program + +Here is the complete `main.cpp`, including both record declarations: + +```{literalinclude} ../_snippets/tutorials/catalog.cpp +:language: cpp +``` + +You have now written and read nested records in a named collection. Continue with +[Load and update application settings](settings.md) to read documents that omit +some fields and to handle invalid input. diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md new file mode 100644 index 0000000..428ce1b --- /dev/null +++ b/docs/tutorials/index.md @@ -0,0 +1,45 @@ +# Tutorials + +
Learn by building
+ +## From your first record to a working XML application + +Start with a small round trip, then build a catalogue, load application settings, +and define a custom product code. Each lesson gives you complete code, steps you +can run, and exact output to check along the way. + +
+

New to SerialXML? Start with one record.
The first lesson sets up your CMake application and takes you from a C++ struct to XML and back.

+Start your first round trip → +
+ +## Your learning path + +Follow the lessons in order. After the first lesson, reuse the same CMake project +and change `main.cpp` as each project directs. You can revisit any lesson when +you want to practice that part of the mapping. + +
    +
  1. 01Your first XML round tripBuild your application, write a Person, and restore its values.Project setup · Serialization · Deserialization
  2. +
  3. 02Shape an XML documentTurn age into an attribute, rename a child, and add named hobby items.Annotations · XML names · Repeated items
  4. +
  5. 03Build a book catalogueCombine nested records and a collection, then read every book back.Nested objects · Collections · Round trips
  6. +
  7. 04Load and update application settingsKeep defaults, update current values, and report invalid input.Optional fields · In-place updates · Errors
  8. +
  9. 05Create a custom product codeWrite SKU-42, reconstruct its value, and reject an unexpected prefix.Custom formatting · Input conversion · Validation
  10. +
+ +## Before you begin + +You need basic C++, GCC 16.1+ with reflection support, CMake 4.3.3+, and Ninja. +The repository's dev container provides the toolchain. The first lesson includes +the application configuration; no XML experience is required. + +```{toctree} +:hidden: +:maxdepth: 1 + +../quickstart +schema +catalog +settings +product-code +``` diff --git a/docs/tutorials/product-code.md b/docs/tutorials/product-code.md new file mode 100644 index 0000000..dd70b2c --- /dev/null +++ b/docs/tutorials/product-code.md @@ -0,0 +1,89 @@ +# Create a custom product code + +Give a C++ value the XML representation `SKU-42`, then restore its integer and +reject a code with the wrong prefix. You will build both sides of a custom leaf +conversion rather than asking the reader to guess an inverse format. + +Complete [Load and update application settings](settings.md) first. Keep the +same application configuration and replace `main.cpp` for this project. + +## 1. Write a code with a prefix + +Start with this complete program: + +```{literalinclude} ../_snippets/tutorials/product-code-1.cpp +:language: cpp +``` + +Build and run: + +```bash +cmake --build build +./build/my_app +``` + +Expected output: + +```xml +SKU-42 +``` + +`ProductCode` stores an integer. The formatter returns an owning string with the +prefix, and the annotation on `Product::code` chooses that text representation. +This first program only writes the document. + +## 2. Teach the reader to reverse the format + +Insert this specialization after `format_code`, before the `Product` declaration: + +```cpp +template <> +ProductCode serial_xml::from_string(std::string_view text) { + if (!text.starts_with("SKU-")) { + throw serial_xml::deserialization_error("Expected SKU- code"); + } + return {serial_xml::from_string(text.substr(4))}; +} +``` + +The input conversion checks the prefix and passes the remaining decimal text to +the built-in integer conversion. It must be declared before the first call that +needs it. It receives decoded leaf text, so XML entity handling is already done. + +## 3. Read a valid code and reject an invalid one + +Replace `main` with: + +```{literalinclude} ../_snippets/tutorials/product-code.cpp +:language: cpp +:lines: 23- +``` + +Build and run again. The output, in order, is: + +```xml +SKU-42 +``` + +```text +Restored product: 7; code: 42 +Rejected input: Expected SKU- code +``` + +The successful read recovers the integer 42. The second document contains +`OTHER-42`, so your specialization rejects it with the diagnostic you supplied. + +## Completed program + +```{literalinclude} ../_snippets/tutorials/product-code.cpp +:language: cpp +``` + +Change the prefix in the formatter and reader to `ITEM-`, update the invalid-input +check if needed, and rebuild. Verify that the new XML representation still reads +back to the same integer. This exercise keeps the two conversion directions in +agreement. + +You have completed the learning path: ordinary records, annotations, nested +collections, input defaults, and custom text conversions. Use the +[how-to guides](../how-to/index.md) when applying these techniques to your own schema. diff --git a/docs/tutorials/schema.md b/docs/tutorials/schema.md new file mode 100644 index 0000000..bdcbbfe --- /dev/null +++ b/docs/tutorials/schema.md @@ -0,0 +1,118 @@ +# Shape an XML document + +In this tutorial, you will extend the application from +[your first XML round trip](../quickstart.md). You will create a `person` element +with an age attribute, a renamed food element, and a list of hobbies, then read +it back. Keep the same `CMakeLists.txt` and replace `main.cpp` as instructed below. + +## 1. Name the root and one child + +Keep the imports and replace your `Person` declaration with: + +```cpp +struct [[= serial_xml::name{"person"}]] Person { + int age; + [[= serial_xml::name{"food"}]] std::string favorite_food; +}; +``` + +Replace `main` with this fragment-printing version: + +```cpp +int main() { + const Person original{3, "pizza"}; + const auto xml = serial_xml::to_xml(original, false); + std::println("{}", xml); +} +``` + +Run `cmake --build build` and `./build/my_app`. You should see: + +```xml +3pizza +``` + +Both annotations changed XML names. The C++ member still has the name +`favorite_food`, so the initializer and member access remain the same. + +## 2. Make age an attribute + +Change the age declaration to: + +```cpp +[[= serial_xml::attribute]] int age; +``` + +Build and run again. Your output should be: + +```xml +pizza +``` + +The age is now carried by the opening tag. The food remains a child element. + +## 3. Add named repeated items + +Add this member after `favorite_food`: + +```cpp +[[= serial_xml::iter{"hobby"}]] std::vector hobbies; +``` + +Change the initializer in `main` to: + +```cpp +const Person original{3, "pizza", {"drawing", "cycling"}}; +``` + +Build and run. You should see two `hobby` elements inside the `hobbies` wrapper: + +```xml +pizzadrawingcycling +``` + +## 4. Read the shaped document + +Add these lines after printing `xml`: + +```cpp +const auto restored = serial_xml::from_xml(xml); +std::println("Age: {}; food: {}; hobbies: {}", restored.age, + restored.favorite_food, restored.hobbies.size()); +``` + +Build and run once more. The last line should be: + +```text +Age: 3; food: pizza; hobbies: 2 +``` + +The reader uses the same names, attribute placement, and item names as the writer. + +## Completed program + +```cpp +import std; +import serial_xml; + +struct [[= serial_xml::name{"person"}]] Person { + [[= serial_xml::attribute]] int age; + [[= serial_xml::name{"food"}]] std::string favorite_food; + [[= serial_xml::iter{"hobby"}]] std::vector hobbies; +}; + +int main() { + const Person original{3, "pizza", {"drawing", "cycling"}}; + const auto xml = serial_xml::to_xml(original, false); + std::println("{}", xml); + const auto restored = serial_xml::from_xml(xml); + std::println("Age: {}; food: {}; hobbies: {}", restored.age, + restored.favorite_food, restored.hobbies.size()); +} +``` + +## Continue learning + +You have shaped individual fields and repeated items. Continue with +[Build a book catalogue](catalog.md) to combine these choices with nested records +and reconstruct a whole collection. diff --git a/docs/tutorials/settings.md b/docs/tutorials/settings.md new file mode 100644 index 0000000..f54b2fd --- /dev/null +++ b/docs/tutorials/settings.md @@ -0,0 +1,99 @@ +# Load and update application settings + +Build a settings reader that accepts a missing theme, retains an existing theme +during an update, and reports invalid numeric input. You will finish by saving +the current settings back to XML. + +Complete [Build a book catalogue](catalog.md) first, or use an application prepared +in the first tutorial. Keep its `CMakeLists.txt` and replace `main.cpp` as shown. + +## 1. Load a complete document + +Start with this program: + +```{literalinclude} ../_snippets/tutorials/settings-1.cpp +:language: cpp +``` + +Build and run from your application directory: + +```bash +cmake --build build +./build/my_app +``` + +Expected output: + +```text +Volume: 20; theme: dark +``` + +The root name is `settings`. Both children are present, so they replace the +initial values. The `theme` initializer alone does not make that input field +optional. + +## 2. Keep a default when the theme is missing + +Add `optional` to the theme member: + +```cpp +[[= serial_xml::optional]] std::string theme = "light"; +``` + +Replace `main` with: + +```{literalinclude} ../_snippets/tutorials/settings-2.cpp +:language: cpp +:lines: 9- +``` + +Build and run again: + +```text +Initial: volume 20; theme light +Updated: volume 35; theme dark +``` + +The first document omits `theme`, so the new object keeps `light`. You then set +the theme to `dark`. The in-place update changes the volume but retains that +current theme because the second document also omits it. + +## 3. Reject invalid input and save your settings + +Replace `main` with the following version: + +```{literalinclude} ../_snippets/tutorials/settings.cpp +:language: cpp +:lines: 9- +``` + +This reads the invalid document into a separate new object. Its failed conversion +does not update `preferences`. The final call writes the current settings. + +Build and run. The output, in order, is: + +```text +Initial: volume 20; theme light +Updated: volume 35; theme dark +Rejected input: Invalid or out-of-range numeric value: loud +``` + +```xml +35dark +``` + +The XML contains both values. The `optional` annotation permits omission on +input; it does not omit an initialized string when writing. + +## Completed program + +```{literalinclude} ../_snippets/tutorials/settings.cpp +:language: cpp +``` + +Try changing `loud` to `60`. The valid read will print that volume, while the +saved `preferences` will still have volume 35 because the new object was separate. +You have learned to distinguish loading a new value from updating existing state. + +Continue with [Create a custom product code](product-code.md) to define your own +text conversion instead of using a built-in numeric reader. diff --git a/docs/xml-input.md b/docs/xml-input.md new file mode 100644 index 0000000..18a1eb6 --- /dev/null +++ b/docs/xml-input.md @@ -0,0 +1,205 @@ +# XML input reference + +This page specifies the XML syntax and reconstruction rules accepted by +`serial_xml::from_xml`. Function signatures are in the [API reference](api.rst). +For task-oriented examples, see [Deserialize XML](deserialization.md). + +## Supported syntax + +| Input feature | Behavior | +| --- | --- | +| XML version and encoding | UTF-8 XML 1.0 only. A UTF-8 BOM is accepted. | +| Declaration | Optional; when present, starts with `version="1.0"`. Optional `encoding` must be UTF-8 (case insensitive), followed by optional `standalone="yes"` or `"no"`. | +| Elements | Exactly one root; matching opening/closing names and self-closing elements are accepted. | +| Attributes | Single- or double-quoted values; XML whitespace separates attributes. | +| Comments and processing instructions | Accepted around the root and within elements. Ignored for member reconstruction. The processing-instruction target `xml` is reserved. | +| CDATA | Accepted within elements. Scalar tagged strings concatenate CDATA and ordinary text. | +| Predefined entities | `&`, `<`, `>`, `"`, and `'`. | +| Character references | Decimal `A` and hexadecimal `A`; decoded code points must be valid XML 1.0 characters. | +| Names | Valid XML 1.0 names, including Unicode, underscores, and literal namespace prefixes. | +| Namespace processing | Names are matched literally. Namespace declarations are not resolved; `a:item` and `b:item` remain different names. | +| Nesting | At most 256 element levels, counting the root as the first level. | +| DTDs and external entities | Unsupported. No external resource is loaded. | + +The parser rejects malformed or overlong UTF-8, forbidden XML characters, +mismatched tags, unterminated structures, invalid entities or character references, +malformed comments, multiple roots, and trailing non-miscellaneous content. These +failures throw `serial_xml::deserialization_error`; parser diagnostics include a +byte position. + +## Name matching and member selection + +The expected root name is selected in this order: + +1. A nonempty `fixed_name` argument. +2. A `name` annotation on the selected schema (the target type when no external + schema is specified). +3. The actual target type's identifier, or its class-template identifier. + +A root-name mismatch throws `deserialization_error`. An invalid expected root name +throws `std::invalid_argument`. + +Selected fields and getters use their member identifier unless `name` overrides +it. Attributes read an attribute with that name; ordinary members read a child +element. External schemas select direct accessible members by identifier and supply +their annotations. Their placeholder types do not determine the actual conversion +type. Unselected and skipped fields keep their existing or initialized values. + +| Member kind | Input behavior | +| --- | --- | +| Assignable data member | Convert its XML value and assign it. | +| Const getter with a matching setter | Convert using the getter's value type and invoke the setter. Getter annotations define the XML representation. | +| Getter without a matching setter | Ignore it during input. It may still participate in output. | +| Method annotated `setter` | Convert using its single parameter type and invoke it. Its own annotations define the XML representation. | +| Nonassignable data member | Compile-time error unless skipped or unselected. | + +Automatic setter matching accepts the same member name with one parameter, or the +following naming conventions: `value()` → `set_value(T)`, `get_value()` → +`set_value(T)`, and `getValue()` → `setValue(T)`. A candidate must be accessible, +non-static, non-const, callable on an lvalue, and have exactly one parameter of +the getter's value type after removing references, cv qualifiers, and aliases. +Multiple matching candidates cause a compile-time error. + +An explicit `setter` requires an accessible non-static one-parameter method. By +default, `set_value` reads `value`, `setValue` reads `value`, and other method names +are used literally. `name` overrides that default. An explicitly selected setter +takes precedence over automatic reconstruction through its matching getter, so +the method is invoked once. Setter annotations can reside on an external schema; +placeholder methods do not require definitions. Setters do not participate in +serialization. + +## Missing and empty values + +The explicit `optional` annotation has precedence over the natural absence rules +of `std::optional` and `exclude_on_empty`. + +| Member when absent | Value-returning `from_xml(xml)` | In-place `from_xml(object, xml)` | +| --- | --- | --- | +| Ordinary required member | Throws `deserialization_error`. | Throws `deserialization_error`. | +| Member or setter annotated `optional` | Keeps its initializer; setter is not called. | Keeps its current value; setter is not called. | +| `std::optional` without `optional` annotation | Assigns `std::nullopt`. | Assigns `std::nullopt`, replacing any previous value. | +| Automatically iterated range annotated `exclude_on_empty`, without `optional` or explicit `iter` | Assigns a value-initialized range (empty for dynamic ranges). | Assigns a value-initialized range, replacing any previous contents. | +| Skipped field or unselected schema field | Keeps its initialized value. | Keeps its current value. | +| Read-only getter | Ignored. | Ignored. | + +An empty element counts as present. It yields an empty string or range, while an +empty numeric element fails conversion. Present fixed-size arrays require the +exact number of elements. An absent `std::optional` differs from a present +empty element: a present value attempts `T`'s conversion and produces an engaged +optional if conversion succeeds. + +Raw ranges have no wrapper to require. With no matching item elements they produce +an empty range, or keep their value if annotated `optional`. Empty fixed-size +arrays still fail the size check. The `exclude_on_empty` absence rule applies to +automatically iterated ranges; an explicit `iter` wrapper remains required unless +the member is also annotated `optional`. + +## Order, unknown content, and duplicates + +* XML attribute and ordinary child-element order is independent of C++ member + order. Members are assigned or passed to setters in schema order. +* Unknown attributes and child elements of an object are ignored after the entire + XML document has been parsed and validated. +* Duplicate XML attributes are rejected, including attributes not selected by the + schema. +* Duplicate matched singleton elements or range wrappers are rejected. Unknown + repeated child elements are ignored. +* Repeated matching item tags in an iterated range are the range's elements and + preserve their input order. +* A wrapped range rejects unexpected item tags and non-whitespace direct text. + A raw range selects its matching item tags from the parent and ignores unrelated + siblings and direct text. +* Scalar tagged values reject nested child elements. Ordinary text and CDATA within + a scalar tagged element are concatenated; comments and processing instructions + do not contribute text. + +## Text and attribute normalization + +| Source text | Reconstructed text | +| --- | --- | +| Literal CRLF or CR in element text and CDATA | LF. | +| Literal tab, LF, CR, or CRLF in an attribute | Space; CRLF becomes one space. | +| Character references such as ` `, ` `, and ` ` | The referenced character, without literal-whitespace normalization. | +| XML entities in ordinary text or attributes | Their decoded characters. | +| Entity-looking text within CDATA | Literal text; entities are not decoded. | + +Strings retain whitespace after XML normalization. Arithmetic and boolean +conversion trims surrounding spaces, tabs, carriage returns, and line feeds. +Pretty printing can introduce additional direct text between child elements; use +compact `to_xml` output when round-tripping raw scalar fields or when every text +node matters. + +On output, SerialXML rejects forbidden XML 1.0 characters and malformed UTF-8, +including custom formatter output, with `std::invalid_argument`. Text carriage +returns and attribute tabs, newlines, and carriage returns are written as character +references to preserve their values. Annotation names are validated at compile +time; invalid runtime root overrides throw `std::invalid_argument`. + +## Leaf conversion representations + +Non-unpacked leaves use `serial_xml::from_string(std::string_view)` after XML +decoding. Calling `from_string` directly does not parse XML or decode entities. + +| Type | Accepted representation | +| --- | --- | +| `std::string` | Complete decoded text, retaining whitespace. | +| `bool` | `true`, `false`, `1`, or `0` after whitespace trimming. | +| Arithmetic types | A complete `std::from_chars` representation after trimming. Empty, invalid, trailing, or out-of-range input fails. | +| `char` | Numeric character value for a scalar leaf, matching `to_xml`. Quoted character syntax is also handled inside formatted containers. | +| `std::optional` | A present textual value converts to `T` and creates an engaged optional. | +| Pair or tuple | `(value, value)` with the exact tuple arity. | +| Owning sequence | `[value, value]`. | +| Owning set | `{value, value}`. | +| Owning map | `{key: value, key: value}`. | + +Container representations support nested containers and quoted strings, including +escaped quotes, backslashes, newline/tab/carriage-return escapes, and braced Unicode +escapes. Fixed arrays require exactly their size; fixed-capacity containers reject +too many items. Writable forward lists and valarrays are supported. + +XML item iteration is automatic for `vector`, `array`, `inplace_vector`, `deque`, +`forward_list`, and `valarray`. `iter` selects item iteration for other owning +ranges; `no_iter` selects formatted container text. Borrowed views such as `span`, +`string_view`, and pointers have no built-in reconstruction. A custom implementation +must provide an appropriate storage lifetime. + +Custom leaf types require an explicit `from_string` specialization declared +before the first call that needs it. Unpacked classes use reflection instead of +leaf conversion. + +`format` controls output representation; deserialization does not invert a custom +formatter or lossy format specifier. Decimal zero padding works with built-in +numeric conversion. Prefixes, hexadecimal output, alignment fill, and other +representations require matching conversion. Lost precision cannot be recovered. +See [custom leaf conversion](deserialization.md) for an implementation example. + +## Raw text and CDATA + +Raw scalar fields consume the parent's direct ordinary text, excluding CDATA. +More than one selected raw scalar field cannot be separated and throws +`deserialization_error`. Raw ranges consume matching item tags directly from the +parent. Unpacked objects and automatically handled optionals retain tagged +representations even with `raw`, following serialization precedence. + +CDATA fields consume the parent's CDATA sections in member order. Default +arithmetic output uses tagged numeric elements even with `cdata`, and input follows +that representation. The writer splits `]]>` across CDATA sections and emits +carriage returns as character references; the reader rejoins those canonical +continuations. + +Adjacent fields can be ambiguous when their boundary resembles a canonical split: +one value ends in `]]` and the next starts in `>`. Optional CDATA fields and +externally split CDATA can also make boundaries ambiguous. Tagged string fields +provide an unambiguous boundary and can contain both ordinary text and CDATA. + +## Failure and update behavior + +Malformed XML, missing required input, duplicate selected elements, and built-in +conversion failures throw `serial_xml::deserialization_error`. Exceptions from +custom conversions, assignment operators, or setters propagate. + +The value-returning overload starts from `T{}`. The in-place overload first copies +the input so a field that owns the XML can safely be replaced. Both parse the +complete XML document before updating members. Conversion, assignment, and setter +execution happen afterward in schema order; a failure can leave an existing +object partially updated. No rollback is performed.