Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions include/ro-crate.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,17 @@ namespace rocrate {
throw std::invalid_argument("Cannot set '@id' property directly. "
"Use ROCrate::addEntity to assign an ID.");

// Check property is not duplicate - ignore if so
auto it = properties_->find(property);
if (it != properties_->end()) {
for (const auto& existingValue : it->second) {
if (existingValue.value == value && existingValue.type == valueType) {
// Duplicate found, ignore
return;
}
}
}

// Add the value to the property in the properties map
(*properties_)[property].push_back({value, valueType});
}
Expand Down Expand Up @@ -194,6 +205,15 @@ namespace rocrate {
*/
void writeOut(const std::string& path);

/**
* Reads an RO-Crate from a JSON file at the specified path and populates
* the current RO-Crate instance with the entities and context from the file.
*
* @param path The file path from which to read the RO-Crate JSON.
* @throw std::runtime_error if there is an error reading from the file or parsing the JSON.
*/
void readIn(const std::string& path);

private:
nlohmann::json serializeEntity(
const std::string& id,
Expand All @@ -208,6 +228,11 @@ namespace rocrate {
const PropertyValue& value
) const;

void updateProperties(
Entity& entity,
const nlohmann::json& jsonEntity
);

EntityRegister entities_;
std::map<std::string, std::string> context_;
};
Expand Down Expand Up @@ -301,6 +326,90 @@ namespace rocrate {
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);

// 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());
}

// Validate the RO-Crate structure
if (!inCrate.contains("@context") || !inCrate.contains("@graph")) {
throw std::runtime_error("Invalid RO-Crate structure in file: " + path);
}

// Upsert the context entries from the JSON representation into the RO-Crate
if (inCrate["@context"].is_array()) {
for (const auto& contextEntry : inCrate["@context"]) {
if (contextEntry.is_object()) {
for (const auto& [key, value] : contextEntry.items()) {
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"]) {
// 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.");
}
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.");
}
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);
} catch (const std::runtime_error&) {
// Entity does not exist, create a new one
entity = Entity({entityType});
updateProperties(entity, entityJson);
addEntity(entityId, entity);
}
}
}

inline void ROCrate::updateProperties(Entity& entity, const nlohmann::json& entityJson) {
// Iterate over the properties of the entity and add them to the Entity object
for (const auto& [property, value] : entityJson.items()) {
if (property == "@id") {
continue; // Skip the @id property
}

// Handle the property value based on its type (array, object, or literal)
if (value.is_array()) {
// If array, iterate over the items and add them to the Entity object
for (const auto& item : value) {
if (item.is_object() && item.contains("@id")) {
entity.set(property, item["@id"].get<std::string>(), ValueType::Reference);
} else {
entity.set(property, item.get<std::string>(), ValueType::Literal);
}
}
} else if (value.is_object() && value.contains("@id")) {
entity.set(property, value["@id"].get<std::string>(), ValueType::Reference);
} else {
entity.set(property, value.get<std::string>(), ValueType::Literal);
}
}
}

inline nlohmann::json ROCrate::serializePropertyValue(
const PropertyValue& value
) const {
Expand Down
22 changes: 22 additions & 0 deletions tests/integration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,25 @@ TEST_CASE("RO-Crate which focuses on multiple Contextual Entities", "[integratio
outputPath
);
}

TEST_CASE("ReadIn RO-Crate roundtrips successfully", "[integration]")
{
// Create a new RO-Crate object
ROCrate crate;

// Read in the RO-Crate from a file (into the crate object)
const std::string inputPath =
std::string(TEST_SOURCE_DIR) + "/tests/fixtures/minimal-example-of-ro-crate.json";
crate.readIn(inputPath);

// Write back out
const std::string outputPath =
std::string(TEST_SOURCE_DIR) + "/ro-crate-metadata.json";
crate.writeOut(outputPath);

// Assert that the input and output files are equal by ID
REQUIRE_RO_CRATE_FILE_EQUAL_BY_ID(
inputPath,
outputPath
);
}
29 changes: 29 additions & 0 deletions tests/unit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ TEST_CASE("Rejects empty link property names", "[unit]") {
);
}

TEST_CASE("Ignore duplicate key-value pairs", "[unit]") {
Entity entity({"Thing"});

REQUIRE_NOTHROW(entity.set("name", "value"));
REQUIRE_NOTHROW(entity.set("name", "value")); // Should not throw, but also not add a duplicate

SUCCEED("No exceptions thrown, can't currently assert on entity values as they are not exposed in the API");
}

TEST_CASE("Ensure failure if entity is linked before being assigned an ID", "[unit]") {
// Create the person, but do not add to a crate (so no @id yet)
Entity alice({"Person"});
Expand Down Expand Up @@ -156,3 +165,23 @@ TEST_CASE("Add entity rejects entities with duplicate IDs", "[unit]") {
aliceDuplicate.set("name", "Alice Duplicate");
REQUIRE_THROWS_AS(crate.addEntity("#alice", aliceDuplicate), std::runtime_error);
}

TEST_CASE("Write out RO-Crate to JSON-LD without throwing", "[unit]") {
rocrate::ROCrate crate;

Entity alice({"Person"});
alice.set("name", "Alice");
crate.addEntity("#alice", alice);

REQUIRE_NOTHROW(crate.writeOut("output/ro-crate-metadata.json"));
}

TEST_CASE("Read in RO-Crate from JSON-LD without throwing", "[unit]") {
rocrate::ROCrate crate;

REQUIRE_NOTHROW(
crate.readIn(
std::string(TEST_SOURCE_DIR) + "/tests/fixtures/minimal-example-of-ro-crate.json"
)
);
}
Loading