diff --git a/.github/workflows/cmake-single-platform.yml b/.github/workflows/cmake-single-platform.yml index bba50e6..1735809 100644 --- a/.github/workflows/cmake-single-platform.yml +++ b/.github/workflows/cmake-single-platform.yml @@ -7,6 +7,7 @@ on: branches: [ "main" ] pull_request: branches: [ "main" ] + workflow_dispatch: env: # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) @@ -22,6 +23,9 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install Catch2 + run: sudo apt-get install catch2 + - name: Configure CMake # Configure CMake in a 'build' subdirectory run: cmake -B ${{github.workspace}}/build @@ -33,5 +37,5 @@ jobs: working-directory: ${{github.workspace}}/build # Execute tests defined by the CMake configuration. # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail - run: ctest --test-dir build --rerun-failed --output-on-failure + run: ctest --rerun-failed --output-on-failure diff --git a/.github/workflows/make-docs.yml b/.github/workflows/make-docs.yml new file mode 100644 index 0000000..120cf92 --- /dev/null +++ b/.github/workflows/make-docs.yml @@ -0,0 +1,50 @@ +name: Build Doxide Docs + +on: + push: + branches: [ "main" ] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +jobs: + build-docs: + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.10' + + - name: Install Doxide and MkDocs Material + run: | + echo 'deb http://download.indii.org/deb resolute main' | sudo tee /etc/apt/sources.list.d/indii.org.list + curl -fsSL https://download.indii.org/deb/Release.key | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/indii.org.gpg > /dev/null + sudo apt update + sudo apt install doxide + pip install mkdocs mkdocs-material + + - name: Build Doxide Documentation + run: | + doxide build + + - name: Build MkDocs Site + run: | + mkdocs build + + - name: Configure GitHub Pages + uses: actions/configure-pages@v5 + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./site + + - name: Deploy to GitHub Pages + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index 635f7f6..537b1d2 100644 --- a/README.md +++ b/README.md @@ -80,16 +80,6 @@ int main() { } ``` -## Explaination - -### Philosophy of approach - -This header tries to avoid JSON manipulation as much as possible. It focuses -on a bare bones approach to RO-Crates; modelling an RO-Crate a flat list of entities, -where each entity is a flat list of properties. The library does not attempt to -model the RO-Crate specification in a more complex way, such as modelling the -relationships between entities. - ## How-To ### Create an RO-Crate @@ -158,6 +148,68 @@ write the RO-Crate to. This will write out the `ro-crate-metadata.json` file. crate.writeOut("./routput/");o ``` +## Explaination + +These notes are mostly aimed at developers who want to understand the design of +this library, but may be of interest to users as well. + +### Philosophy of approach + +This header tries to avoid JSON manipulation as much as possible. It focuses +on a bare bones approach to RO-Crates; modelling an RO-Crate a flat list of entities, +where each entity is a flat list of properties. The library does not attempt to +model the RO-Crate specification in a more complex way, such as modelling the +relationships between entities. + +### Entity lifecycle + +When an entity is created, it is not automatically added to the RO-Crate. The user +must explicitly add the entity to the RO-Crate using the `addEntity` method. + +When an entity is added to the RO-Crate, it is stored in a map of entities. + +For maximum flexiblity, an already added entity may still be updated on the +original entity object. The RO-Crate shares the same entity object, so any changes +made to the original entity will be reflected in the RO-Crate. + +Worked example: + +```cpp +ROCrate crate; + +Entity alice({"Person"}); +alice.set("name", "Alice"); + +crate.addEntity("#alice", alice); + +/* Crate state: +{ + "#alice": { + "type": ["Person"], + "name": "Alice" + } +} +*/ + +// Add an additional property to the crate entity +Entity crateAlice = crate.getEntity("#alice"); +crateAlice.set("description", "One of hopefully many Contextual Entities"); + +// Add an additional property to the original entity object +alice.set("occupation", "Software Engineer"); + +/* Crate state: +{ + "#alice": { + "type": ["Person"], + "name": "Alice", + "description": "One of hopefully many Contextual Entities", + "occupation": "Software Engineer" + } +} +*/ +``` + ## Reference -Doxygen or similar reference documentation to follow. +Reference documentation generated by [doxide](doxide.org) is available [here](http://esciencelab.org.uk/ro-crate-cpp/). diff --git a/docs/javascripts/mathjax.js b/docs/javascripts/mathjax.js new file mode 100644 index 0000000..080801e --- /dev/null +++ b/docs/javascripts/mathjax.js @@ -0,0 +1,16 @@ +window.MathJax = { + tex: { + inlineMath: [["\\(", "\\)"]], + displayMath: [["\\[", "\\]"]], + processEscapes: true, + processEnvironments: true + }, + options: { + ignoreHtmlClass: ".*|", + processHtmlClass: "arithmatex" + } +}; + +document$.subscribe(() => { + MathJax.typesetPromise() +}) diff --git a/docs/javascripts/tablesort.js b/docs/javascripts/tablesort.js new file mode 100644 index 0000000..6a5afcf --- /dev/null +++ b/docs/javascripts/tablesort.js @@ -0,0 +1,6 @@ +document$.subscribe(function() { + var tables = document.querySelectorAll("article table:not([class])") + tables.forEach(function(table) { + new Tablesort(table) + }) +}) diff --git a/docs/overrides/partials/copyright.html b/docs/overrides/partials/copyright.html new file mode 100644 index 0000000..473f830 --- /dev/null +++ b/docs/overrides/partials/copyright.html @@ -0,0 +1,17 @@ + diff --git a/docs/stylesheets/doxide.css b/docs/stylesheets/doxide.css new file mode 100644 index 0000000..e6a9ccf --- /dev/null +++ b/docs/stylesheets/doxide.css @@ -0,0 +1,58 @@ +:root { + --md-admonition-icon--variable: url('data:image/svg+xml;charset=utf-8,'); + --md-admonition-icon--function: url('data:image/svg+xml;charset=utf-8,'); + --md-admonition-icon--typedef: url('data:image/svg+xml;charset=utf-8,'); + --md-admonition-icon--concept: url('data:image/svg+xml;charset=utf-8,'); + --md-admonition-icon--macro: url('data:image/svg+xml;charset=utf-8,'); +} + +.md-typeset .admonition.variable, .md-typeset details.variable, +.md-typeset .admonition.function, .md-typeset details.function, +.md-typeset .admonition.typedef, .md-typeset details.typedef, +.md-typeset .admonition.concept, .md-typeset details.concept, +.md-typeset .admonition.macro, .md-typeset details.macro { + border-color: var(--md-default-fg-color--lighter); +} + +.md-typeset .variable > .admonition-title, .md-typeset .variable > summary, +.md-typeset .function > .admonition-title, .md-typeset .function > summary, +.md-typeset .typedef > .admonition-title, .md-typeset .typedef > summary, +.md-typeset .concept > .admonition-title, .md-typeset .concept > summary, +.md-typeset .macro > .admonition-title, .md-typeset .macro > summary { + background-color: var(--md-default-bg-color); +} + +.md-typeset .variable > .admonition-title::before, +.md-typeset .variable > summary::before { + background-color: var(--md-default-fg-color--light); + -webkit-mask-image: var(--md-admonition-icon--variable); + mask-image: var(--md-admonition-icon--variable); +} + +.md-typeset .function > .admonition-title::before, +.md-typeset .function > summary::before { + background-color: var(--md-default-fg-color--light); + -webkit-mask-image: var(--md-admonition-icon--function); + mask-image: var(--md-admonition-icon--function); +} + +.md-typeset .typedef > .admonition-title::before, +.md-typeset .typedef > summary::before { + background-color: var(--md-default-fg-color--light); + -webkit-mask-image: var(--md-admonition-icon--typedef); + mask-image: var(--md-admonition-icon--typedef); +} + +.md-typeset .concept > .admonition-title::before, +.md-typeset .concept > summary::before { + background-color: var(--md-default-fg-color--light); + -webkit-mask-image: var(--md-admonition-icon--concept); + mask-image: var(--md-admonition-icon--concept); +} + +.md-typeset .macro > .admonition-title::before, +.md-typeset .macro > summary::before { + background-color: var(--md-default-fg-color--light); + -webkit-mask-image: var(--md-admonition-icon--macro); + mask-image: var(--md-admonition-icon--macro); +} diff --git a/doxide.yaml b/doxide.yaml new file mode 100644 index 0000000..e6815ed --- /dev/null +++ b/doxide.yaml @@ -0,0 +1,4 @@ +title: ro-crate-cpp +description: A header only library for creating RO-Crates in C++ +files: + - "include/ro-crate.hpp" diff --git a/include/ro-crate.hpp b/include/ro-crate.hpp index 0331ac0..3449cdc 100644 --- a/include/ro-crate.hpp +++ b/include/ro-crate.hpp @@ -26,35 +26,64 @@ namespace rocrate { using Properties = std::map>; + /** + * Represents an entity in the RO-Crate. + * An entity can have multiple types and properties, where each property can have multiple values. + * Properties can be set as either literal values or references to other entities. + */ class Entity { public: Entity() = delete; + + /** + * Constructs an Entity with the specified types. + * + * @param types A vector of strings representing the types of the entity. + * @throw std::invalid_argument if the types vector is empty. + */ explicit Entity(std::vector types); ~Entity() = default; - + + /** + * Sets a property-value pair for the entity. + * + * @param property The name of the property to set. + * @param value The value to assign to the property. + * @param valueType The type of the value (Literal or Reference). + * @throw std::invalid_argument if the property name or value is empty, or if attempting to set '@id' directly. + */ void set( Property property, Value value, ValueType type = ValueType::Literal ); + + /** + * Sets a property to reference another entity. + * + * @param property The name of the property to set. + * @param entity The entity to reference. + * @throw std::invalid_argument if the property name is empty. + * @throw std::runtime_error if the referenced entity does not have an '@id' property set. + */ void set(Property property, const Entity& entity); private: friend class ROCrate; + /** + * Assigns an '@id' to the entity. + * + * @param id The identifier to assign to the entity. + * @throw std::invalid_argument if the id is empty. + */ void assignId(const std::string& id); std::shared_ptr properties_; }; inline Entity::Entity(std::vector types) : properties_(std::make_shared()) { - /** - * @brief Constructs an Entity with the specified types. - * - * @param types A vector of strings representing the types of the entity. - * @throws std::invalid_argument if the types vector is empty. - */ // Validate types (reject empty) if ( types.empty() ) { @@ -69,15 +98,6 @@ namespace rocrate { } inline void Entity::set(Property property, Value value, ValueType valueType) { - /** - * @brief Sets a property-value pair for the entity. - * - * @param property The name of the property to set. - * @param value The value to assign to the property. - * @param valueType The type of the value (Literal or Reference). - * @throws std::invalid_argument if the property name or value is empty, or if attempting to set '@id' directly. - */ - // Validate property and value if (property.empty()) throw std::invalid_argument("Property name cannot be empty."); @@ -92,15 +112,6 @@ namespace rocrate { } inline void Entity::set(Property property, const Entity& entity) { - /** - * @brief Sets a property to reference another entity. - * - * @param property The name of the property to set. - * @param entity The entity to reference. - * @throws std::invalid_argument if the property name is empty. - * @throws std::runtime_error if the referenced entity does not have an '@id' property set. - */ - // Validate property name if (property.empty()) { throw std::invalid_argument("Property name cannot be empty."); @@ -116,13 +127,6 @@ namespace rocrate { } inline void Entity::assignId(const std::string& id) { - /** - * @brief Assigns an '@id' to the entity. - * - * @param id The identifier to assign to the entity. - * @throws std::invalid_argument if the id is empty. - */ - // Validate id if (id.empty()) { throw std::invalid_argument("Entity ID cannot be empty."); @@ -138,12 +142,47 @@ namespace rocrate { using EntityRegister = std::map; + /** + * Represents an RO-Crate, a structured collection of entities and metadata. + * + * The ROCrate class manages a collection of entities, including a root metadata entity and a root dataset entity. + * It provides methods to add entities, retrieve entities by their identifiers, and manage the relationships between entities. + */ class ROCrate { public: + /** + * Constructs an RO-Crate with a root metadata entity and a root dataset entity. + * + * The constructor initializes the RO-Crate with an empty entity register, creates the root metadata entity + * (ro-crate-metadata.json) and the root dataset entity, and adds the root dataset entity to the root metadata entity. + */ ROCrate(); + /** + * Adds an entity to the RO-Crate's entity register with the specified id. + * + * @param id The identifier for the entity. + * @param entity The entity to add to the RO-Crate. + * @throw std::invalid_argument if the id is empty. + * @throw std::runtime_error if an entity with the given id already exists in the RO-Crate. + */ void addEntity(const std::string& id, Entity& entity); + + /** + * Retrieves an entity from the RO-Crate's entity register by its id. + * + * @param id The identifier of the entity to retrieve. + * @return A reference to the entity with the specified id. + * @throw std::runtime_error if no entity with the given id is found in the RO-Crate. + */ Entity& getEntity(const std::string& id); + + /** + * Serializes the RO-Crate to a JSON file at the specified path. + * + * @param path The file path where the RO-Crate JSON will be written. + * @throw std::runtime_error if there is an error writing to the file. + */ void writeOut(const std::string& path); private: @@ -164,13 +203,6 @@ namespace rocrate { }; inline ROCrate::ROCrate() { - /** - * @brief Constructs an RO-Crate with a root metadata entity and a root dataset entity. - * - * The constructor initializes the RO-Crate with an empty entity register, creates the root metadata entity - * (ro-crate-metadata.json) and the root dataset entity, and adds the root dataset entity to the root metadata entity. - */ - // Initialise the RO-Crate with an empty entity register entities_ = {}; @@ -192,15 +224,6 @@ namespace rocrate { } inline void ROCrate::addEntity(const std::string& id, Entity& entity) { - /** - * @brief Adds an entity to the RO-Crate's entity register with the specified id. - * - * @param id The identifier for the entity. - * @param entity The entity to add to the RO-Crate. - * @throws std::invalid_argument if the id is empty. - * @throws std::runtime_error if an entity with the given id already exists in the RO-Crate. - */ - // Validate the id (reject empty) if (id.empty()) { throw std::invalid_argument("Entity ID cannot be empty."); @@ -219,14 +242,6 @@ namespace rocrate { } inline Entity& ROCrate::getEntity(const std::string& id) { - /** - * @brief Retrieves an entity from the RO-Crate's entity register by its id. - * - * @param id The identifier of the entity to retrieve. - * @return A reference to the entity with the specified id. - * @throws std::runtime_error if no entity with the given id is found in the RO-Crate. - */ - auto it = entities_.find(id); if (it == entities_.end()) { throw std::runtime_error("Entity with id '" + id + "' not found in the RO-Crate."); @@ -234,14 +249,7 @@ namespace rocrate { return it->second; } - inline void ROCrate::writeOut(const std::string& path) { - /** - * @brief Serializes the RO-Crate to a JSON file at the specified path. - * - * @param path The file path where the RO-Crate JSON will be written. - * @throws std::runtime_error if there is an error writing to the file. - */ - + inline void ROCrate::writeOut(const std::string& path) { nlohmann::json outCrate = { {"@context", "https://w3id.org/ro/crate/1.1/context"}, {"@graph", nlohmann::json::array()} diff --git a/mkdocs.yaml b/mkdocs.yaml new file mode 100644 index 0000000..312f354 --- /dev/null +++ b/mkdocs.yaml @@ -0,0 +1,45 @@ +site_name: ro-crate-cpp +site_description: A header only library for creating RO-Crates in C++ +theme: + name: material + custom_dir: docs/overrides + features: + - navigation.indexes + palette: + # Palette toggle for light mode + - scheme: default + primary: red + accent: red + toggle: + icon: material/brightness-7 + name: Switch to dark mode + + # Palette toggle for dark mode + - scheme: slate + primary: red + accent: red + toggle: + icon: material/brightness-4 + name: Switch to light mode + +markdown_extensions: + - def_list + - attr_list + - admonition + - pymdownx.details + - pymdownx.superfences + - pymdownx.arithmatex: + generic: true + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg +plugins: + - search +extra_css: + - stylesheets/doxide.css +extra_javascript: + - javascripts/mathjax.js + - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js + - https://cdn.jsdelivr.net/npm/tablesort@5.3.0/src/tablesort.min.js + - https://cdn.jsdelivr.net/npm/tablesort@5.3.0/src/sorts/tablesort.number.js + - javascripts/tablesort.js diff --git a/vendor/json.hpp b/vendor/nlohmann/json.hpp similarity index 100% rename from vendor/json.hpp rename to vendor/nlohmann/json.hpp