Repository navigation
Implementation of suggestions arising from pmslavin's review #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,15 +2,85 @@ | |
|
|
||
| #include <nlohmann/json.hpp> | ||
|
|
||
| #include <format> | ||
| #include <fstream> | ||
| #include <memory> | ||
| #include <stdexcept> | ||
| #include <string> | ||
| #include <string_view> | ||
| #include <map> | ||
| #include <unordered_map> | ||
| #include <vector> | ||
|
|
||
| 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: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this different from |
||
| 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 | ||
| // --------------------------------------------------------------------------- | ||
|
|
@@ -24,7 +94,7 @@ namespace rocrate { | |
| ValueType type; | ||
| }; | ||
|
|
||
| using Properties = std::map<Property, std::vector<PropertyValue>>; | ||
| using Properties = std::unordered_map<Property, std::vector<PropertyValue>>; | ||
|
|
||
| /** | ||
| * Represents an entity in the RO-Crate. | ||
|
|
@@ -87,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 | ||
|
|
@@ -100,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); | ||
|
|
@@ -125,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); | ||
|
|
@@ -140,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 | ||
|
|
@@ -151,7 +220,7 @@ namespace rocrate { | |
| // RO-Crate | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| using EntityRegister = std::map<std::string, Entity>; | ||
| using EntityRegister = std::unordered_map<std::string, Entity>; | ||
|
|
||
| /** | ||
| * Represents an RO-Crate, a structured collection of entities and metadata. | ||
|
|
@@ -215,6 +284,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 | ||
|
|
@@ -261,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_.find(id) != entities_.end()) { | ||
| throw std::runtime_error("Entity with id '" + id + "' already exists in the RO-Crate."); | ||
| if (entities_.contains(id)) { | ||
| throw std::runtime_error(std::format(error_message(ErrorCode::EntityAlreadyExists), id)); | ||
| } | ||
|
|
||
| // Add the assigned ID to the Entity itself | ||
|
|
@@ -279,108 +351,118 @@ 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; | ||
| } | ||
|
|
||
| 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 | ||
| context_[key] = value; | ||
| } | ||
|
|
||
| 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(std::format(error_message(ErrorCode::FileOpenWriteFailed), 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) { | ||
| // 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 | ||
| 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(std::string(error_message(ErrorCode::InvalidCrateStructure))); | ||
| } | ||
|
|
||
| // 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<std::string>()); | ||
| crate.addContext(key, value.get<std::string>()); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // 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."); | ||
| throw std::runtime_error(std::string(error_message(ErrorCode::EntityMissingIdField))); | ||
| } | ||
| std::string entityId = entityJson["@id"].get<std::string>(); | ||
|
|
||
| // 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<std::string>(); | ||
|
|
||
| // Try getting the entity from the register, if it exists, otherwise create a new one | ||
| 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&) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Or can the presence of the Entity in the register be tested directly without requiring |
||
| // 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); | ||
| } | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not important and probably harmless either way, but does
FIND_PACKAGE_ARGSdo anything here when an explicitGIT_TAGhas been provided?