diff --git a/README.md b/README.md index 537b1d2..20b8070 100644 --- a/README.md +++ b/README.md @@ -96,8 +96,8 @@ ROCrate crate; ### Create an Entity -To create an entity, instantiate the `Entity` class. You can must provide a type, -or list of types. +To create an entity, instantiate the `Entity` class. You must provide a type, +or can provide a list of types. ```cpp // Single type @@ -139,16 +139,28 @@ Entity alice({"Person"}); crate.addEntity("#alice", alice); ``` +### Add additional context to an RO-Crate + +To add additional context to an RO-Crate, use the `addContext` method. The first +argument is the id of the context to add. The second is the context link itself +which must be non-empty. + +```cpp +ROCrate crate; + +crate.addContext("txc", "https://purl.archive.org/language-data-commons/terms#"); +``` + ### Write out an RO-Crate To write out an RO-Crate, use the `writeOut` method. The argument is the path to write the RO-Crate to. This will write out the `ro-crate-metadata.json` file. ```cpp -crate.writeOut("./routput/");o +crate.writeOut("./routput/ro-crate-metadata.json"); ``` -## Explaination +## Explanation 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. diff --git a/include/ro-crate.hpp b/include/ro-crate.hpp index 3449cdc..63018bd 100644 --- a/include/ro-crate.hpp +++ b/include/ro-crate.hpp @@ -177,6 +177,15 @@ namespace rocrate { */ Entity& getEntity(const std::string& id); + /** + * Adds a context entry to the RO-Crate's root metadata entity. + * + * @param key The context key to add. + * @param value The context value associated with the key. + * @throw std::invalid_argument if the key or value is empty. + */ + void addContext(const std::string& key, const std::string& value); + /** * Serializes the RO-Crate to a JSON file at the specified path. * @@ -200,6 +209,7 @@ namespace rocrate { ) const; EntityRegister entities_; + std::map context_; }; inline ROCrate::ROCrate() { @@ -210,7 +220,7 @@ namespace rocrate { Entity rootEntity({"CreativeWork"}); rootEntity.set( "conformsTo", - "https://w3id.org/ro/crate/1.1", + "https://w3id.org/ro/crate/1.3", ValueType::Reference ); addEntity("ro-crate-metadata.json", rootEntity); @@ -249,12 +259,35 @@ namespace rocrate { return it->second; } - inline void ROCrate::writeOut(const std::string& path) { + inline void ROCrate::addContext(const std::string& key, const std::string& value) { + // Validate the context + if (key.empty()) { + throw std::invalid_argument("Context key cannot be empty."); + } + if (value.empty()) { + throw std::invalid_argument("Context value cannot be empty."); + } + + // Add the context entry to the RO-Crate's root metadata entity + context_[key] = value; + } + + inline void ROCrate::writeOut(const std::string& path) { + // Create the JSON representation of the RO-Crate nlohmann::json outCrate = { - {"@context", "https://w3id.org/ro/crate/1.1/context"}, + {"@context", "https://w3id.org/ro/crate/1.3/context"}, {"@graph", nlohmann::json::array()} }; + // Add the context entries to the JSON representation + if (!context_.empty()) { + outCrate["@context"] = nlohmann::json::array(); + outCrate["@context"].push_back("https://w3id.org/ro/crate/1.3/context"); + for (const auto& [key, value] : context_) { + outCrate["@context"].push_back({{key, value}}); + } + } + // Iterate over the entities, conver to json and append to the graph for (const auto& [id, entity] : entities_) { outCrate["@graph"].push_back(serializeEntity(id, entity)); diff --git a/tests/fixtures/example-with-file-author-location.json b/tests/fixtures/example-with-file-author-location.json deleted file mode 100644 index 91fca09..0000000 --- a/tests/fixtures/example-with-file-author-location.json +++ /dev/null @@ -1,47 +0,0 @@ -{ "@context": "https://w3id.org/ro/crate/1.1/context", - "@graph": [ - - { - "@type": "CreativeWork", - "@id": "ro-crate-metadata.json", - "conformsTo": {"@id": "https://w3id.org/ro/crate/1.1"}, - "about": {"@id": "./"}, - "description": "RO-Crate Metadata File Descriptor (this file)" - }, - { - "@id": "./", - "@type": "Dataset", - "name": "Example RO-Crate", - "description": "The RO-Crate Root Data Entity", - "hasPart": [ - {"@id": "data1.txt"}, - {"@id": "data2.txt"} - ] - }, - - - { - "@id": "data1.txt", - "@type": "File", - "description": "One of hopefully many Data Entities", - "author": {"@id": "#alice"}, - "contentLocation": {"@id": "http://sws.geonames.org/8152662/"} - }, - { - "@id": "data2.txt", - "@type": "File" - }, - - { - "@id": "#alice", - "@type": "Person", - "name": "Alice", - "description": "One of hopefully many Contextual Entities" - }, - { - "@id": "http://sws.geonames.org/8152662/", - "@type": "Place", - "name": "Catalina Park" - } - ] -} diff --git a/tests/fixtures/example-with-file-directory.json b/tests/fixtures/example-with-file-directory.json new file mode 100644 index 0000000..6dff969 --- /dev/null +++ b/tests/fixtures/example-with-file-directory.json @@ -0,0 +1,45 @@ +{ "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@type": "CreativeWork", + "@id": "ro-crate-metadata.json", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"}, + "about": {"@id": "./"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Example Dataset", + "datePublished": "2016-02-01", + "author": {"@id": "https://orcid.org/0000-0003-4953-0830"}, + "license": "CC-BY", + "hasPart": [ + { + "@id": "cp7glop.ai" + }, + { + "@id": "lots_of_little_files/" + } + ] + }, + { + "@id": "cp7glop.ai", + "@type": "File", + "name": "Diagram showing trend to increase", + "contentSize": "383766", + "description": "Illustrator file for Glop Pot", + "encodingFormat": "application/pdf" + }, + { + "@id": "lots_of_little_files/", + "@type": "Dataset", + "name": "Too many files", + "description": "This directory contains many small files -- the name of the file is a date in YYYY-MM-DD.csv, each file contains daily temperature readings, sampled hourly for the Glop Pot cave." + }, + { + "@id": "https://orcid.org/0000-0003-4953-0830", + "@type": "Person", + "name": "Michael Lake" + } + ] +} diff --git a/tests/fixtures/example-with-web-resources.json b/tests/fixtures/example-with-web-resources.json index ec015b1..4be3b86 100644 --- a/tests/fixtures/example-with-web-resources.json +++ b/tests/fixtures/example-with-web-resources.json @@ -1,9 +1,9 @@ -{ "@context": "https://w3id.org/ro/crate/1.1/context", +{ "@context": "https://w3id.org/ro/crate/1.3/context", "@graph": [ { "@type": "CreativeWork", "@id": "ro-crate-metadata.json", - "conformsTo": {"@id": "https://w3id.org/ro/crate/1.1"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"}, "about": {"@id": "./"} }, { @@ -16,7 +16,7 @@ { "@id": "https://zenodo.org/record/3541888/files/ro-crate-1.0.0.pdf" } - ] + ] }, { "@id": "survey-responses-2019.csv", diff --git a/tests/fixtures/minimal-example-of-ro-crate.json b/tests/fixtures/minimal-example-of-ro-crate.json index 4f004f5..9ef0779 100644 --- a/tests/fixtures/minimal-example-of-ro-crate.json +++ b/tests/fixtures/minimal-example-of-ro-crate.json @@ -1,27 +1,36 @@ -{ "@context": "https://w3id.org/ro/crate/1.1/context", +{ "@context": "https://w3id.org/ro/crate/1.3/context", "@graph": [ { - "@type": "CreativeWork", "@id": "ro-crate-metadata.json", - "conformsTo": {"@id": "https://w3id.org/ro/crate/1.1"}, - "about": {"@id": "./"} + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} }, { "@id": "./", - "identifier": "https://doi.org/10.4225/59/59672c09f4a4b", "@type": "Dataset", + "identifier": {"@id": "https://doi.org/10.4225/59/59672c09f4a4b"}, + "cite-as": "https://doi.org/10.4225/59/59672c09f4a4b", "datePublished": "2017", "name": "Data files associated with the manuscript:Effects of facilitated family case conferencing for ...", "description": "Palliative care planning for nursing home residents with advanced dementia ...", - "license": {"@id": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/"} + "license": {"@id": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/"}, + "creditText": "Agar, M. et al., 2017. Data supporting \"Effects of facilitated family case conferencing for advanced dementia: A cluster randomised clinical trial\". https://doi.org/10.4225/59/59672c09f4a4b" + }, + { + "@id": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/", + "@type": "CreativeWork", + "description": "This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Australia License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/3.0/au/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.", + "identifier": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/", + "name": "Attribution-NonCommercial-ShareAlike 3.0 Australia (CC BY-NC-SA 3.0 AU)" }, { - "@id": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/", - "@type": "CreativeWork", - "description": "This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Australia License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/3.0/au/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.", - "identifier": "https://creativecommons.org/licenses/by-nc-sa/3.0/au/", - "name": "Attribution-NonCommercial-ShareAlike 3.0 Australia (CC BY-NC-SA 3.0 AU)" - } + "@id": "https://doi.org/10.4225/59/59672c09f4a4b", + "@type": "PropertyValue", + "propertyID": "https://registry.identifiers.org/registry/doi", + "value": "doi:10.4225/59/59672c09f4a4b", + "url": "https://doi.org/10.4225/59/59672c09f4a4b" + } ] } diff --git a/tests/fixtures/ro-crate-which-focus-on-multiple-contextual-entities.json b/tests/fixtures/ro-crate-which-focus-on-multiple-contextual-entities.json new file mode 100644 index 0000000..a876a15 --- /dev/null +++ b/tests/fixtures/ro-crate-which-focus-on-multiple-contextual-entities.json @@ -0,0 +1,50 @@ +{ "@context": [ + "https://w3id.org/ro/crate/1.3/context", + {"txc": "https://purl.archive.org/language-data-commons/terms#"} + ], + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "description": "RO-Crate Metadata File Descriptor (this file)", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"}, + "about": {"@id": "./"} + }, + { + "@id": "./", + "@type": "Dataset", + "description": "This is an experimental language data ontology based on OLAC terms for use in the ATAP and LDaCA projects", + "name": "Language Data Ontology", + "mentions": [ + {"@id": "txc:Annotation"}, + {"@id": "txc:CollectionEvent"} + ] + }, + { + "@id": "txc:Annotation", + "@type": "rdfs:Class", + "name": "Annotation", + "sameAs": "http://www.language-archives.org/REC/type-20020628.html#annotation", + "rdfs:comment": "The resource includes information which annotates some other linguistic record.", + "rdfs:label": "Annotation", + "rdfs:subClassOf": { + "@id": "schema:CreativeWork" + } + }, + { + "@id": "txc:CollectionEvent", + "@type": "rdfs:Class", + "name": "CollectionEvent", + "rdfs:comment": "A description of an event at which one or more PrimaryTexts were captured, e.g. as video or audio", + "rdfs:label": "CollectionEvent", + "rdfs:subClassOf": [ + { + "@id": "schema:Event" + }, + { + "@id": "schema:CreateAction" + } + ] + } + ] +} diff --git a/tests/integration.cpp b/tests/integration.cpp index b30b1fe..6080384 100644 --- a/tests/integration.cpp +++ b/tests/integration.cpp @@ -5,18 +5,21 @@ using rocrate::Entity; using rocrate::ROCrate; +using rocrate::ValueType; -// https://www.researchobject.org/ro-crate/specification/1.1/root-data-entity#minimal-example-of-ro-crate +// REF: https://www.researchobject.org/ro-crate/specification/1.3/root-data-entity.html#minimal-example-of-ro-crate +// FIXTURE: tests/fixtures/minimal-example-of-ro-crate.json TEST_CASE("Minimal RO-Crate", "[integration]") { ROCrate crate; // Add metadata to root data entity Entity rootData = crate.getEntity("./"); - rootData.set("identifier", "https://doi.org/10.4225/59/59672c09f4a4b"); + rootData.set("cite-as", "https://doi.org/10.4225/59/59672c09f4a4b"); rootData.set("datePublished", "2017"); rootData.set("name", "Data files associated with the manuscript:Effects of facilitated family case conferencing for ..."); rootData.set("description", "Palliative care planning for nursing home residents with advanced dementia ..."); + rootData.set("creditText", "Agar, M. et al., 2017. Data supporting \"Effects of facilitated family case conferencing for advanced dementia: A cluster randomised clinical trial\". https://doi.org/10.4225/59/59672c09f4a4b"); // Create the license entity Entity license({"CreativeWork"}); @@ -26,6 +29,14 @@ TEST_CASE("Minimal RO-Crate", "[integration]") crate.addEntity("https://creativecommons.org/licenses/by-nc-sa/3.0/au/", license); + // Create the DOI entry + Entity doi({"PropertyValue"}); + doi.set("propertyID", "https://registry.identifiers.org/registry/doi"); + doi.set("value", "doi:10.4225/59/59672c09f4a4b"); + doi.set("url", "https://doi.org/10.4225/59/59672c09f4a4b"); + crate.addEntity("https://doi.org/10.4225/59/59672c09f4a4b", doi); + rootData.set("identifier", doi); + // Add license to root data entity rootData.set("license", license); @@ -41,61 +52,58 @@ TEST_CASE("Minimal RO-Crate", "[integration]") ); } -// https://www.researchobject.org/ro-crate/specification/1.1/appendix/jsonld -TEST_CASE("Example with file, author, location", "[integration]") +// REF: https://www.researchobject.org/ro-crate/specification/1.3/data-entities.html#example-attached-ro-crate-package +// FIXTURE: tests/fixtures/example-with-file-directory.json +TEST_CASE("Example with file and directory", "[integration]") { - ROCrate crate; - - // Add description to the root metadata entity (ro-crate-metadata.json) - Entity root = crate.getEntity("ro-crate-metadata.json"); - root.set("description", "RO-Crate Metadata File Descriptor (this file)"); - REQUIRE_NOTHROW(crate.getEntity("ro-crate-metadata.json")); - - // Add name, description to the root data entity (./) - Entity rootData = crate.getEntity("./"); - rootData.set("name", "Example RO-Crate"); - rootData.set("description", "The RO-Crate Root Data Entity"); - REQUIRE_NOTHROW(crate.getEntity("./")); - - // Create the person - Entity alice({"Person"}); - alice.set("name", "Alice"); - alice.set("description", "One of hopefully many Contextual Entities"); - crate.addEntity("#alice", alice); - REQUIRE_NOTHROW(crate.getEntity("#alice")); - - // Create the place - Entity catalinaPark({"Place"}); - catalinaPark.set("name", "Catalina Park"); - crate.addEntity("http://sws.geonames.org/8152662/", catalinaPark); - REQUIRE_NOTHROW(crate.getEntity("http://sws.geonames.org/8152662/")); - - // Create two datasets - Entity data1({"File"}); - data1.set("description", "One of hopefully many Data Entities"); - data1.set("author", alice); - data1.set("contentLocation", catalinaPark); - crate.addEntity("data1.txt", data1); - rootData.set("hasPart", data1); - REQUIRE_NOTHROW(crate.getEntity("data1.txt")); - - Entity data2({"File"}); - crate.addEntity("data2.txt", data2); - rootData.set("hasPart", data2); - REQUIRE_NOTHROW(crate.getEntity("data2.txt")); - - // Write out - const std::string outputPath = - std::string(TEST_SOURCE_DIR) + "/ro-crate-metadata.json"; - crate.writeOut(outputPath); - - REQUIRE_RO_CRATE_FILE_EQUAL_BY_ID( - std::string(TEST_SOURCE_DIR) + - "/tests/fixtures/example-with-file-author-location.json", - outputPath - ); + ROCrate crate; + + // Add metadata to root data entity + Entity rootData = crate.getEntity("./"); + rootData.set("name", "Example Dataset"); + rootData.set("datePublished", "2016-02-01"); + rootData.set("license", "CC-BY"); + + // Create the author entity + Entity author({"Person"}); + author.set("name", "Michael Lake"); + crate.addEntity("https://orcid.org/0000-0003-4953-0830", author); + rootData.set("author", author); + + // Create the file entity + Entity file({"File"}); + file.set("name", "Diagram showing trend to increase"); + file.set("contentSize", "383766"); + file.set("description", "Illustrator file for Glop Pot"); + file.set("encodingFormat", "application/pdf"); + crate.addEntity("cp7glop.ai", file); + + // Create the directory entity + Entity directory({"Dataset"}); + directory.set("name", "Too many files"); + directory.set("description", "This directory contains many small files -- the name of the file is a date in YYYY-MM-DD.csv, each file contains daily temperature readings, sampled hourly for the Glop Pot cave."); + + crate.addEntity("lots_of_little_files/", directory); + + // Add file and directory to root data entity + rootData.set("hasPart", file); + rootData.set("hasPart", directory); + + // Write out + const std::string outputPath = + std::string(TEST_SOURCE_DIR) + "/ro-crate-metadata.json"; + crate.writeOut(outputPath); + + REQUIRE_RO_CRATE_FILE_EQUAL_BY_ID( + std::string(TEST_SOURCE_DIR) + + "/tests/fixtures/example-with-file-directory.json", + outputPath + ); + } +// REF: https://www.researchobject.org/ro-crate/specification/1.3/data-entities.html#web-based-data-entities +// FIXTURE: tests/fixtures/example-with-web-resources.json TEST_CASE("Example with web resources", "[integration]") { ROCrate crate; @@ -132,3 +140,55 @@ TEST_CASE("Example with web resources", "[integration]") ); } + +// REF: https://www.researchobject.org/ro-crate/specification/1.3/crate-focus.html (RO-Crates which focus on multiple Contextual Entities) +// FIXTURE: tests/fixtures/ro-crate-which-focus-on-multiple-contextual-entities.json +TEST_CASE("RO-Crate which focuses on multiple Contextual Entities", "[integration]") +{ + ROCrate crate; + + // Add metadata to the RO-Crate metadata file descriptor + Entity metadataFileDescriptor = crate.getEntity("ro-crate-metadata.json"); + metadataFileDescriptor.set("description", "RO-Crate Metadata File Descriptor (this file)"); + + // Add metadata to root data entity + Entity rootData = crate.getEntity("./"); + rootData.set("name", "Language Data Ontology"); + rootData.set("description", "This is an experimental language data ontology based on OLAC terms for use in the ATAP and LDaCA projects"); + + // Add additional context + crate.addContext("txc", "https://purl.archive.org/language-data-commons/terms#"); + + // Create the first contextual entity + Entity annotation({"rdfs:Class"}); + annotation.set("name", "Annotation"); + annotation.set("sameAs", "http://www.language-archives.org/REC/type-20020628.html#annotation"); + annotation.set("rdfs:comment", "The resource includes information which annotates some other linguistic record."); + annotation.set("rdfs:label", "Annotation"); + annotation.set("rdfs:subClassOf", "schema:CreativeWork", ValueType::Reference); + crate.addEntity("txc:Annotation", annotation); + + // Create the second contextual entity + Entity collectionEvent({"rdfs:Class"}); + collectionEvent.set("name", "CollectionEvent"); + collectionEvent.set("rdfs:comment", "A description of an event at which one or more PrimaryTexts were captured, e.g. as video or audio"); + collectionEvent.set("rdfs:label", "CollectionEvent"); + collectionEvent.set("rdfs:subClassOf", "schema:Event", ValueType::Reference); + collectionEvent.set("rdfs:subClassOf", "schema:CreateAction", ValueType::Reference); + crate.addEntity("txc:CollectionEvent", collectionEvent); + + // Add mentions to root data entity + rootData.set("mentions", annotation); + rootData.set("mentions", collectionEvent); + + // Write out + const std::string outputPath = + std::string(TEST_SOURCE_DIR) + "/ro-crate-metadata.json"; + crate.writeOut(outputPath); + + REQUIRE_RO_CRATE_FILE_EQUAL_BY_ID( + std::string(TEST_SOURCE_DIR) + + "/tests/fixtures/ro-crate-which-focus-on-multiple-contextual-entities.json", + outputPath + ); +} diff --git a/tests/unit.cpp b/tests/unit.cpp index c9b5d08..ca94674 100644 --- a/tests/unit.cpp +++ b/tests/unit.cpp @@ -112,6 +112,37 @@ TEST_CASE("Crate and builder observe shared entity updates", "[unit]") { SUCCEED("No exceptions thrown, can't currently assert on entity values as they are not exposed in the API"); } +TEST_CASE("Context can be added without error", "[unit]") { + rocrate::ROCrate crate; + + REQUIRE_NOTHROW(crate.addContext("example", "https://example.com/context.jsonld")); +} + +TEST_CASE("Duplicate context doesn't throw", "[unit]") { + rocrate::ROCrate crate; + + REQUIRE_NOTHROW(crate.addContext("example", "https://example.com/context.jsonld")); + REQUIRE_NOTHROW(crate.addContext("example", "https://example.com/context.jsonld")); + +} + +TEST_CASE("Add context throws if context is empty", "[unit]") { + rocrate::ROCrate crate; + + REQUIRE_THROWS_AS( + crate.addContext("", "https://example.com/context.jsonld"), + std::invalid_argument + ); + REQUIRE_THROWS_AS(crate.addContext("example", ""), std::invalid_argument); +} + +TEST_CASE("Add entity throws if ID is empty", "[unit]") { + rocrate::ROCrate crate; + Entity alice({"Person"}); + + REQUIRE_THROWS_AS(crate.addEntity("", alice), std::invalid_argument); +} + TEST_CASE("Add entity rejects entities with duplicate IDs", "[unit]") { // Create a crate and add an entity rocrate::ROCrate crate;