From 4429433c7dde84c678d9c02670eaecaa56248f8f Mon Sep 17 00:00:00 2001 From: Oliver Woolland Date: Mon, 12 Oct 2026 10:12:16 +0100 Subject: [PATCH 1/3] Automatically fetch catch2 lib! --- CMakeLists.txt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ad36610..dec79e6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,8 +1,9 @@ -cmake_minimum_required(VERSION 3.20) +cmake_minimum_required(VERSION 3.24) project(ro-crate-cpp LANGUAGES CXX) include(CTest) +include(FetchContent) add_library(ro-crate-cpp INTERFACE) @@ -24,7 +25,12 @@ target_link_libraries(example ) if(BUILD_TESTING) - find_package(Catch2 3 REQUIRED) + FetchContent_Declare(Catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + GIT_TAG v3.16.0 + FIND_PACKAGE_ARGS 3 + ) + FetchContent_MakeAvailable(Catch2) add_executable(ro-crate-cpp_tests tests/integration.cpp From 96353c790de0713519d0dc119f5140a050e93331 Mon Sep 17 00:00:00 2001 From: Oliver Woolland Date: Mon, 12 Oct 2026 12:01:37 +0100 Subject: [PATCH 2/3] Refactor readin/writeout to use nlohmann --- include/ro-crate.hpp | 70 ++++++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/include/ro-crate.hpp b/include/ro-crate.hpp index 2d6f5d0..ab970ad 100644 --- a/include/ro-crate.hpp +++ b/include/ro-crate.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include namespace rocrate { @@ -24,7 +25,7 @@ namespace rocrate { ValueType type; }; - using Properties = std::map>; + using Properties = std::unordered_map>; /** * Represents an entity in the RO-Crate. @@ -151,7 +152,7 @@ namespace rocrate { // RO-Crate // --------------------------------------------------------------------------- - using EntityRegister = std::map; + using EntityRegister = std::unordered_map; /** * Represents an RO-Crate, a structured collection of entities and metadata. @@ -215,6 +216,9 @@ namespace rocrate { void readIn(const std::string& path); private: + friend void to_json(nlohmann::json& j, const ROCrate& crate); + friend void from_json(const nlohmann::json& j, ROCrate& crate); + nlohmann::json serializeEntity( const std::string& id, const Entity& entity @@ -265,7 +269,7 @@ namespace rocrate { } // Check if the entity with the given id already exists in the register - if (entities_.find(id) != entities_.end()) { + if (entities_.contains(id)) { throw std::runtime_error("Entity with id '" + id + "' already exists in the RO-Crate."); } @@ -298,32 +302,37 @@ namespace rocrate { } inline void ROCrate::writeOut(const std::string& path) { + nlohmann::json outCrate = *this; + + + // Write the JSON representation of the RO-Crate to a file + std::ofstream outFile(path); + if (!outFile) + throw std::runtime_error("Failed to open file for writing: " + path); + + outFile << outCrate.dump(4); // Pretty print with 4 spaces indentation + } + + inline void to_json(nlohmann::json& j, const ROCrate& crate) { // Create the JSON representation of the RO-Crate - nlohmann::json outCrate = { + j = { {"@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}}); + if (!crate.context_.empty()) { + j["@context"] = nlohmann::json::array(); + j["@context"].push_back("https://w3id.org/ro/crate/1.3/context"); + for (const auto& [key, value] : crate.context_) { + j["@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)); + for (const auto& [id, entity] : crate.entities_) { + j["@graph"].push_back(crate.serializeEntity(id, entity)); } - - // Write the JSON representation of the RO-Crate to a file - std::ofstream outFile(path); - if (!outFile) - throw std::runtime_error("Failed to open file for writing: " + path); - - outFile << outCrate.dump(4); // Pretty print with 4 spaces indentation } inline void ROCrate::readIn(const std::string& path) { @@ -340,24 +349,29 @@ namespace rocrate { throw std::runtime_error("Failed to parse JSON from file: " + path + ". Error: " + e.what()); } + // Validate the RO-Crate structure and populate the current instance + from_json(inCrate, *this); + } + + inline void from_json(const nlohmann::json& j, ROCrate& crate) { // Validate the RO-Crate structure - if (!inCrate.contains("@context") || !inCrate.contains("@graph")) { - throw std::runtime_error("Invalid RO-Crate structure in file: " + path); + if (!j.contains("@context") || !j.contains("@graph")) { + throw std::runtime_error("Invalid RO-Crate structure: missing '@context' or '@graph' fields."); } // Upsert the context entries from the JSON representation into the RO-Crate - if (inCrate["@context"].is_array()) { - for (const auto& contextEntry : inCrate["@context"]) { + if (j["@context"].is_array()) { + for (const auto& contextEntry : j["@context"]) { if (contextEntry.is_object()) { for (const auto& [key, value] : contextEntry.items()) { - addContext(key, value.get()); + crate.addContext(key, value.get()); } } } } // Iterate over the entities in the JSON representation and add them to the RO-Crate - for (const auto& entityJson : inCrate["@graph"]) { + for (const auto& entityJson : j["@graph"]) { // Validate that the entity has an '@id' field if (!entityJson.contains("@id")) { throw std::runtime_error("Entity in RO-Crate JSON is missing '@id' field."); @@ -374,13 +388,13 @@ namespace rocrate { Entity entity({"Thing"}); // Default type, will be overridden if entity exists try { // Existing entity - entity = getEntity(entityId); - updateProperties(entity, entityJson); + entity = crate.getEntity(entityId); + crate.updateProperties(entity, entityJson); } catch (const std::runtime_error&) { // Entity does not exist, create a new one entity = Entity({entityType}); - updateProperties(entity, entityJson); - addEntity(entityId, entity); + crate.updateProperties(entity, entityJson); + crate.addEntity(entityId, entity); } } } From de6d8eb7c7030cbf95a3ec1092e20d8935e53eab Mon Sep 17 00:00:00 2001 From: Oliver Woolland Date: Mon, 12 Oct 2026 14:22:35 +0100 Subject: [PATCH 3/3] Refactor error messages into central defintion --- include/ro-crate.hpp | 106 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 87 insertions(+), 19 deletions(-) diff --git a/include/ro-crate.hpp b/include/ro-crate.hpp index ab970ad..3049d70 100644 --- a/include/ro-crate.hpp +++ b/include/ro-crate.hpp @@ -2,16 +2,85 @@ #include +#include #include #include #include #include +#include #include #include #include namespace rocrate { + // --------------------------------------------------------------------------- + // Errors + // --------------------------------------------------------------------------- + + enum class ErrorCode { + TypesEmpty, + PropertyNameEmpty, + PropertyValueEmpty, + IdPropertyDirectSet, + EntityMissingId, + EntityIdEmpty, + EntityAlreadyExists, + EntityNotFound, + ContextKeyEmpty, + ContextValueEmpty, + FileOpenWriteFailed, + FileOpenReadFailed, + FileParseFailed, + InvalidCrateStructure, + EntityMissingIdField, + EntityMissingTypeField + }; + + /** + * Returns the message template for the given error code. + * Templates with placeholders are substituted via std::format at the throw site. + */ + constexpr std::string_view error_message(ErrorCode code) { + switch (code) { + case ErrorCode::TypesEmpty: + return "Entity must have at least one type."; + case ErrorCode::PropertyNameEmpty: + return "Property name cannot be empty."; + case ErrorCode::PropertyValueEmpty: + return "Property value cannot be empty."; + case ErrorCode::IdPropertyDirectSet: + return "Cannot set '@id' property directly. " + "Use ROCrate::addEntity to assign an ID."; + case ErrorCode::EntityMissingId: + return "Entity does not have an '@id' property set."; + case ErrorCode::EntityIdEmpty: + return "Entity ID cannot be empty."; + case ErrorCode::EntityAlreadyExists: + return "Entity with id '{}' already exists in the RO-Crate."; + case ErrorCode::EntityNotFound: + return "Entity with id '{}' not found in the RO-Crate."; + case ErrorCode::ContextKeyEmpty: + return "Context key cannot be empty."; + case ErrorCode::ContextValueEmpty: + return "Context value cannot be empty."; + case ErrorCode::FileOpenWriteFailed: + return "Failed to open file for writing: {}"; + case ErrorCode::FileOpenReadFailed: + return "Failed to open file for reading: {}"; + case ErrorCode::FileParseFailed: + return "Failed to parse JSON from file: {}. Error: {}"; + case ErrorCode::InvalidCrateStructure: + return "Invalid RO-Crate structure: missing '@context' or '@graph' fields."; + case ErrorCode::EntityMissingIdField: + return "Entity in RO-Crate JSON is missing '@id' field."; + case ErrorCode::EntityMissingTypeField: + return "Entity in RO-Crate JSON is missing '@type' field."; + } + + return "Unknown error"; + } + // --------------------------------------------------------------------------- // Entity // --------------------------------------------------------------------------- @@ -88,7 +157,7 @@ namespace rocrate { // Validate types (reject empty) if ( types.empty() ) { - throw std::invalid_argument("Entity must have at least one type."); + throw std::invalid_argument(std::string(error_message(ErrorCode::TypesEmpty))); } // Set the types of the entity @@ -101,12 +170,11 @@ namespace rocrate { inline void Entity::set(Property property, Value value, ValueType valueType) { // Validate property and value if (property.empty()) - throw std::invalid_argument("Property name cannot be empty."); + throw std::invalid_argument(std::string(error_message(ErrorCode::PropertyNameEmpty))); if (value.empty()) - throw std::invalid_argument("Property value cannot be empty."); + throw std::invalid_argument(std::string(error_message(ErrorCode::PropertyValueEmpty))); if (property == "@id") - throw std::invalid_argument("Cannot set '@id' property directly. " - "Use ROCrate::addEntity to assign an ID."); + throw std::invalid_argument(std::string(error_message(ErrorCode::IdPropertyDirectSet))); // Check property is not duplicate - ignore if so auto it = properties_->find(property); @@ -126,13 +194,13 @@ namespace rocrate { inline void Entity::set(Property property, const Entity& entity) { // Validate property name if (property.empty()) { - throw std::invalid_argument("Property name cannot be empty."); + throw std::invalid_argument(std::string(error_message(ErrorCode::PropertyNameEmpty))); } // Check if the entity has an '@id' property set const auto id = entity.properties_->find("@id"); if (id == entity.properties_->end() || id->second.empty()) - throw std::runtime_error("Entity does not have an '@id' property set."); + throw std::runtime_error(std::string(error_message(ErrorCode::EntityMissingId))); // Add JSON reference to the entity's '@id' to the property in the properties map set(property, id->second.front().value, ValueType::Reference); @@ -141,7 +209,7 @@ namespace rocrate { inline void Entity::assignId(const std::string& id) { // Validate id if (id.empty()) { - throw std::invalid_argument("Entity ID cannot be empty."); + throw std::invalid_argument(std::string(error_message(ErrorCode::EntityIdEmpty))); } // Assign the '@id' property to the entity's properties map @@ -265,12 +333,12 @@ namespace rocrate { inline void ROCrate::addEntity(const std::string& id, Entity& entity) { // Validate the id (reject empty) if (id.empty()) { - throw std::invalid_argument("Entity ID cannot be empty."); + throw std::invalid_argument(std::string(error_message(ErrorCode::EntityIdEmpty))); } // Check if the entity with the given id already exists in the register if (entities_.contains(id)) { - throw std::runtime_error("Entity with id '" + id + "' already exists in the RO-Crate."); + throw std::runtime_error(std::format(error_message(ErrorCode::EntityAlreadyExists), id)); } // Add the assigned ID to the Entity itself @@ -283,7 +351,7 @@ namespace rocrate { inline Entity& ROCrate::getEntity(const std::string& id) { auto it = entities_.find(id); if (it == entities_.end()) { - throw std::runtime_error("Entity with id '" + id + "' not found in the RO-Crate."); + throw std::runtime_error(std::format(error_message(ErrorCode::EntityNotFound), id)); } return it->second; } @@ -291,10 +359,10 @@ namespace rocrate { 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."); + throw std::invalid_argument(std::string(error_message(ErrorCode::ContextKeyEmpty))); } if (value.empty()) { - throw std::invalid_argument("Context value cannot be empty."); + throw std::invalid_argument(std::string(error_message(ErrorCode::ContextValueEmpty))); } // Add the context entry to the RO-Crate's root metadata entity @@ -308,7 +376,7 @@ namespace rocrate { // Write the JSON representation of the RO-Crate to a file std::ofstream outFile(path); if (!outFile) - throw std::runtime_error("Failed to open file for writing: " + path); + throw std::runtime_error(std::format(error_message(ErrorCode::FileOpenWriteFailed), path)); outFile << outCrate.dump(4); // Pretty print with 4 spaces indentation } @@ -339,14 +407,14 @@ namespace rocrate { // Read the JSON representation of the RO-Crate from a file std::ifstream inFile(path); if (!inFile) - throw std::runtime_error("Failed to open file for reading: " + path); + throw std::runtime_error(std::format(error_message(ErrorCode::FileOpenReadFailed), path)); // Check if valid JSON nlohmann::json inCrate; try { inFile >> inCrate; } catch (const nlohmann::json::parse_error& e) { - throw std::runtime_error("Failed to parse JSON from file: " + path + ". Error: " + e.what()); + throw std::runtime_error(std::format(error_message(ErrorCode::FileParseFailed), path, e.what())); } // Validate the RO-Crate structure and populate the current instance @@ -356,7 +424,7 @@ namespace rocrate { inline void from_json(const nlohmann::json& j, ROCrate& crate) { // Validate the RO-Crate structure if (!j.contains("@context") || !j.contains("@graph")) { - throw std::runtime_error("Invalid RO-Crate structure: missing '@context' or '@graph' fields."); + throw std::runtime_error(std::string(error_message(ErrorCode::InvalidCrateStructure))); } // Upsert the context entries from the JSON representation into the RO-Crate @@ -374,13 +442,13 @@ namespace rocrate { for (const auto& entityJson : j["@graph"]) { // Validate that the entity has an '@id' field if (!entityJson.contains("@id")) { - throw std::runtime_error("Entity in RO-Crate JSON is missing '@id' field."); + throw std::runtime_error(std::string(error_message(ErrorCode::EntityMissingIdField))); } std::string entityId = entityJson["@id"].get(); // Get the entity type if (!entityJson.contains("@type")) { - throw std::runtime_error("Entity in RO-Crate JSON is missing '@type' field."); + throw std::runtime_error(std::string(error_message(ErrorCode::EntityMissingTypeField))); } std::string entityType = entityJson["@type"].get();