From fb60faeebe95780ff2efe80b39e40c349085c888 Mon Sep 17 00:00:00 2001 From: woutslabbinck Date: Wed, 21 Jan 2026 15:43:41 +0100 Subject: [PATCH 1/2] feat: add implementation and test for core parsing/serialization for policy, request and report --- documentation/release.md | 3 +- package.json | 2 +- src/evaluator/Atomizer.ts | 2 +- src/index.core.ts | 3 +- src/util/Vocabularies.ts | 1 + src/util/policy/PolicyUtil.ts | 6 +- ...portUtil.ts => ComplianceReportParsing.ts} | 122 +++++++++- .../report/ComplianceReportSerialization.ts | 78 +++++++ src/util/report/ComplianceReportTypes.ts | 28 ++- src/util/request/RequestUtil.ts | 4 +- test/integration/ODRL-newTestCases.test.ts | 10 +- test/unit/util/policy/PolicyUtil.test.ts | 158 +++++++++++++ .../report/ComplianceReportParsing.test.ts | 217 ++++++++++++++++++ .../ComplianceReportSerialization.test.ts | 96 ++++++++ test/unit/util/request/RequestUtil.test.ts | 76 ++++++ tsconfig.json | 1 + 16 files changed, 779 insertions(+), 28 deletions(-) rename src/util/report/{ComplianceReportUtil.ts => ComplianceReportParsing.ts} (51%) create mode 100644 src/util/report/ComplianceReportSerialization.ts create mode 100644 test/unit/util/policy/PolicyUtil.test.ts create mode 100644 test/unit/util/report/ComplianceReportParsing.test.ts create mode 100644 test/unit/util/report/ComplianceReportSerialization.test.ts create mode 100644 test/unit/util/request/RequestUtil.test.ts diff --git a/documentation/release.md b/documentation/release.md index 30561de..74280bb 100644 --- a/documentation/release.md +++ b/documentation/release.md @@ -12,4 +12,5 @@ Steps to follow: - potentially upgrade dependent repositories - ODRL Test Suite at https://github.com/SolidLabResearch/ODRL-Test-Suite - FORCE demo at https://github.com/woutslabbinck/ODRL-Evaluator-Demo - - User-Access Server at https://github.com/SolidLabResearch/user-managed-access \ No newline at end of file + - User-Managed Access Server at https://github.com/SolidLabResearch/user-managed-access + - Policy Conflict Resolver at https://github.com/joachimvh/policy-conflict-resolver \ No newline at end of file diff --git a/package.json b/package.json index 17d3728..283c49e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "odrl-evaluator", - "version": "0.5.1", + "version": "0.6.0", "description": "An open implementation of an ODRL Evaluator that evaluates ODRL policies by generating Compliance Reports", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/src/evaluator/Atomizer.ts b/src/evaluator/Atomizer.ts index 2b236c4..598452d 100644 --- a/src/evaluator/Atomizer.ts +++ b/src/evaluator/Atomizer.ts @@ -7,7 +7,7 @@ const { namedNode, quad } = DataFactory import { PolicyAtomizer } from "odrl-atomizer"; import { ActivationState, PolicyReport, SatisfactionState } from "../util/report/ComplianceReportTypes"; -import { parseComplianceReport } from "../util/report/ComplianceReportUtil"; +import { parseComplianceReport } from "../util/report/ComplianceReportParsing"; import { replaceSubject } from "../util/RDFUtil"; diff --git a/src/index.core.ts b/src/index.core.ts index 5e4d79c..1ca6df4 100644 --- a/src/index.core.ts +++ b/src/index.core.ts @@ -17,6 +17,7 @@ export * from './util/Vocabularies' export * from './util/Prefixes' export * from './util/report/ComplianceReportTypes' -export * from './util/report/ComplianceReportUtil' +export * from './util/report/ComplianceReportSerialization' +export * from './util/report/ComplianceReportParsing' export * from './util/policy/PolicyUtil' export * from './util/request/RequestUtil' \ No newline at end of file diff --git a/src/util/Vocabularies.ts b/src/util/Vocabularies.ts index 93f0863..e072778 100644 --- a/src/util/Vocabularies.ts +++ b/src/util/Vocabularies.ts @@ -3,6 +3,7 @@ import { createVocabulary } from 'rdf-vocabulary'; export const DC = createVocabulary( 'http://purl.org/dc/terms/', 'created', + 'issued', 'title', 'description' ) diff --git a/src/util/policy/PolicyUtil.ts b/src/util/policy/PolicyUtil.ts index c579070..fed3217 100644 --- a/src/util/policy/PolicyUtil.ts +++ b/src/util/policy/PolicyUtil.ts @@ -120,7 +120,7 @@ export function createPolicy(input: { /** * Serialize a Policy into RDF quads. */ -export function makeRDFPolicy(policy: Policy): Quad[] { +export function policyToQuads(policy: Policy): Quad[] { const quads: Quad[] = []; const policyNode = namedNode(policy.identifier); @@ -166,14 +166,14 @@ export function makeRDFPolicy(policy: Policy): Quad[] { const constraintNode = namedNode(c.identifier); quads.push(quad(ruleNode, ODRL.terms.constraint, constraintNode)); - quads.push(...makeRDFConstraint(c)) + quads.push(...constraintToQuads(c)) }); }); return quads; } -export function makeRDFConstraint(constraint: Constraint) { +export function constraintToQuads(constraint: Constraint) { const quads: Quad[] = [] const constraintNode = namedNode(constraint.identifier); quads.push(quad(constraintNode, RDF.terms.type, ODRL.terms.Constraint)); diff --git a/src/util/report/ComplianceReportUtil.ts b/src/util/report/ComplianceReportParsing.ts similarity index 51% rename from src/util/report/ComplianceReportUtil.ts rename to src/util/report/ComplianceReportParsing.ts index 96c2e4c..b792e2a 100644 --- a/src/util/report/ComplianceReportUtil.ts +++ b/src/util/report/ComplianceReportParsing.ts @@ -1,6 +1,6 @@ import { Literal, NamedNode, Quad_Subject, Store } from 'n3'; import { DC, RDF, REPORT } from '../Vocabularies'; -import { ActivationState, PolicyReport, PremiseReport, PremiseReportType, RuleReport, RuleReportType, SatisfactionState } from './ComplianceReportTypes'; +import { ActivationState, AttemptState, DeonticState, PerformanceState, PolicyReport, PremiseReport, PremiseReportType, RuleReport, RuleReportType, SatisfactionState } from './ComplianceReportTypes'; import { Quad } from '@rdfjs/types'; /** @@ -47,14 +47,48 @@ export function parseComplianceReport(identifier: Quad_Subject, store: Store): P */ export function parseRuleReport(identifier: Quad_Subject, store: Store): RuleReport { const premiseNodes = store.getObjects(identifier, REPORT.premiseReport, null) as NamedNode[]; + const activationState = parseSingleState( + store, + identifier, + REPORT.activationState, + ActivationState, + ); + + const attemptState = parseSingleState( + store, + identifier, + REPORT.attemptState, + AttemptState, + ); + + const performanceState = parseSingleState( + store, + identifier, + REPORT.performanceState, + PerformanceState, + ); + + const deonticState = parseSingleState( + store, + identifier, + REPORT.deonticState, + DeonticState, + ); + + return { id: identifier as NamedNode, type: store.getObjects(identifier, RDF.type, null)[0].value as RuleReportType, - activationState: store.getObjects(identifier, REPORT.activationState, null)[0].value as ActivationState, requestedRule: store.getObjects(identifier, REPORT.ruleRequest, null)[0] as NamedNode, rule: store.getObjects(identifier, REPORT.rule, null)[0] as NamedNode, - premiseReport: premiseNodes.map((prem) => parsePremiseReport(prem, store)) - } + premiseReport: premiseNodes.map((prem) => parsePremiseReport(prem, store)), + conditionReport: store.getObjects(identifier, REPORT.conditionReport, null) as NamedNode[], + + activationState, + attemptState, + performanceState, + deonticState + }; } /** @@ -69,9 +103,9 @@ export function parseRuleReport(identifier: Quad_Subject, store: Store): RuleRep * @throws Error if a circular premise report is detected. */ export function parsePremiseReport( - identifier: Quad_Subject, - store: Store, - visited: Set = new Set() + identifier: Quad_Subject, + store: Store, + visited: Set = new Set() ): PremiseReport { const idStr = (identifier as NamedNode).value; if (visited.has(idStr)) { @@ -79,11 +113,77 @@ export function parsePremiseReport( } visited.add(idStr); - const nestedPremises = store.getObjects(identifier, REPORT.PremiseReport, null) as NamedNode[]; - return { + const nestedPremises = store.getObjects(identifier, REPORT.premiseReport, null) as NamedNode[]; + + let premiseReport: PremiseReport = { id: identifier as NamedNode, type: store.getObjects(identifier, RDF.type, null)[0].value as PremiseReportType, - premiseReport: nestedPremises.map((prem) => parsePremiseReport(prem, store)), - satisfactionState: store.getObjects(identifier, REPORT.satisfactionState, null)[0].value as SatisfactionState + premiseReport: nestedPremises.map((prem) => parsePremiseReport(prem, store, visited)), + satisfactionState: parseSingleState(store, identifier, REPORT.satisfactionState, SatisfactionState) + } + + if (premiseReport.type === PremiseReportType.ConstraintReport) { + if (store.getObjects(identifier, REPORT.constraint, null).length !== 1) throw new Error("Expected one constraint"); + const constraintID = store.getObjects(identifier, REPORT.constraint, null)[0] as NamedNode; + premiseReport.constraint = constraintID } + return premiseReport +} + +/** + * Extracts exactly one state value from an N3 store and validates it + * against a provided enum of allowed values. + * + * This helper enforces three guarantees: + * 1. At most one value exists for the given subject–predicate pair. + * 2. If a value exists, it must match one of the allowed enum values. + * 3. Returns `undefined` when no value is present. + * + * The human‑readable label used in error messages is automatically derived + * from the predicate URI by taking the last path or fragment segment. + * + * @template T - An enum-like object whose values represent valid states. + * + * @param store - The N3 store containing RDF quads. + * @param subject - The subject node from which the state is extracted. + * @param predicate - The predicate identifying the state property (as a URI string). + * @param allowed - The enum of allowed values for this state. + * + * @returns The validated state value, or `undefined` if no value is present. + * + * @throws Error + * Thrown when: + * - More than one value is found for the given predicate. + * - The found value does not match any allowed enum value. + */ +function parseSingleState>( + store: Store, + subject: NamedNode | Quad_Subject, + predicate: string, + allowed: T +): T[keyof T] | undefined { + // Derive label from last path or fragment segment + const label = + predicate.split('#').pop()?.split('/').pop() ?? + predicate; + + const objects = store.getObjects(subject, predicate, null); + + if (objects.length > 1) { + throw new Error(`Multiple ${label} values found for ${subject.value}`); + } + + if (objects.length === 0) { + return undefined; + } + + const value = objects[0].value; + const allowedValues = Object.values(allowed); + + if (!allowedValues.includes(value)) { + throw new Error(`Invalid ${label} value for ${subject.value}: ${value}`); + } + + return value as T[keyof T]; } + diff --git a/src/util/report/ComplianceReportSerialization.ts b/src/util/report/ComplianceReportSerialization.ts new file mode 100644 index 0000000..89a4265 --- /dev/null +++ b/src/util/report/ComplianceReportSerialization.ts @@ -0,0 +1,78 @@ + +import { Quad } from '@rdfjs/types'; +import { DataFactory } from 'n3'; +import { DC, RDF, REPORT } from '../Vocabularies'; +import { PolicyReport, PremiseReport, RuleReport } from './ComplianceReportTypes'; +const { quad,namedNode } = DataFactory; + +export function serializeComplianceReport(report: PolicyReport): Quad[] { + const quads: Quad[] = []; + + quads.push(quad(report.id, RDF.terms.type, REPORT.terms.PolicyReport)); + quads.push(quad(report.id, DC.terms.created, report.created)); + if (report.request){ + quads.push(quad(report.id, REPORT.terms.policyRequest, report.request)); + } + quads.push(quad(report.id, REPORT.terms.policy, report.policy)); + + for (const rr of report.ruleReport) { + quads.push(quad(report.id, REPORT.terms.ruleReport, rr.id)); + quads.push(...serializeRuleReport(rr)); + } + + return quads; +} + + +export function serializeRuleReport(ruleReport: RuleReport): Quad[] { + const quads: Quad[] = []; + + quads.push(quad(ruleReport.id, RDF.terms.type, namedNode(ruleReport.type))); + quads.push(quad(ruleReport.id, REPORT.terms.rule, ruleReport.rule)); + if (ruleReport.requestedRule){ + quads.push(quad(ruleReport.id, REPORT.terms.ruleRequest, ruleReport.requestedRule)); + } + + if (ruleReport.attemptState) { + quads.push(quad(ruleReport.id, REPORT.terms.attemptState, namedNode(ruleReport.attemptState))); + } + + if (ruleReport.activationState) { + quads.push(quad(ruleReport.id, REPORT.terms.activationState, namedNode(ruleReport.activationState))); + } + if (ruleReport.performanceState) { + quads.push(quad(ruleReport.id, REPORT.terms.performanceState, namedNode(ruleReport.performanceState))); + } + if (ruleReport.deonticState) { + quads.push(quad(ruleReport.id, REPORT.terms.deonticState, namedNode(ruleReport.deonticState))); + } + + for (const pr of ruleReport.premiseReport) { + quads.push(quad(ruleReport.id, REPORT.terms.premiseReport, pr.id)); + quads.push(...serializePremiseReport(pr)); + } + + for (const condition of ruleReport.conditionReport) { + quads.push(quad(ruleReport.id, REPORT.terms.conditionReport, condition)); + } + + return quads; +} + + +export function serializePremiseReport(premiseReport: PremiseReport): Quad[] { + const quads: Quad[] = []; + + quads.push(quad(premiseReport.id, RDF.terms.type, namedNode(premiseReport.type))); + quads.push(quad(premiseReport.id, REPORT.terms.satisfactionState, namedNode(premiseReport.satisfactionState))); + + for (const nested of premiseReport.premiseReport) { + quads.push(quad(premiseReport.id, REPORT.terms.premiseReport, nested.id)); + quads.push(...serializePremiseReport(nested)); + } + if (premiseReport.constraint) { + quads.push(quad(premiseReport.id, REPORT.terms.constraint, premiseReport.constraint)); + } + + return quads; +} diff --git a/src/util/report/ComplianceReportTypes.ts b/src/util/report/ComplianceReportTypes.ts index 29324b7..7e7f85c 100644 --- a/src/util/report/ComplianceReportTypes.ts +++ b/src/util/report/ComplianceReportTypes.ts @@ -11,17 +11,22 @@ export type PolicyReport = { export type RuleReport = { id: NamedNode; type: RuleReportType; - activationState: ActivationState + attemptState?: AttemptState ; + activationState?: ActivationState + performanceState?: PerformanceState + deonticState?: DeonticState rule: NamedNode; requestedRule: NamedNode; - premiseReport: PremiseReport[] + premiseReport: PremiseReport[]; + conditionReport: NamedNode[]; } export type PremiseReport = { id: NamedNode; type: PremiseReportType; premiseReport: PremiseReport[]; - satisfactionState: SatisfactionState + satisfactionState: SatisfactionState; + constraint?: NamedNode } // is it possible to just use REPORT.namespace + "term"? @@ -46,4 +51,21 @@ export enum PremiseReportType { export enum ActivationState { Active = 'https://w3id.org/force/compliance-report#Active', Inactive = 'https://w3id.org/force/compliance-report#Inactive', +} + +export enum AttemptState { + Attempted = 'https://w3id.org/force/compliance-report#Attempted', + NotAttempted = 'https://w3id.org/force/compliance-report#NotAttempted', +} + +export enum PerformanceState { + Performed = 'https://w3id.org/force/compliance-report#Performed', + Unperformed = 'https://w3id.org/force/compliance-report#Unperformed', + Unknown = 'https://w3id.org/force/compliance-report#Unknown', +} + +export enum DeonticState { + NonSet = 'https://w3id.org/force/compliance-report#NonSet', + Violated = 'https://w3id.org/force/compliance-report#Violated', + Fulfilled = 'https://w3id.org/force/compliance-report#Fulfilled', } \ No newline at end of file diff --git a/src/util/request/RequestUtil.ts b/src/util/request/RequestUtil.ts index d239f16..9caf4e5 100644 --- a/src/util/request/RequestUtil.ts +++ b/src/util/request/RequestUtil.ts @@ -1,7 +1,7 @@ import { DataFactory, Quad, Store } from "n3"; import { createRandomUrn } from "../Util"; import { DC, ODRL, RDF, SOTW } from "../Vocabularies"; -import { Constraint, makeRDFConstraint } from "../policy/PolicyUtil"; +import { Constraint, constraintToQuads } from "../policy/PolicyUtil"; const { namedNode, quad, literal } = DataFactory; /** * Represents an ODRL-Evaluator-style Request. @@ -95,7 +95,7 @@ export function makeRDFRequest(request: Request, permissionID?: string): Quad[] const constraintNode = namedNode(c.identifier); quads.push(quad(permissionNode, SOTW.terms.context, constraintNode)); - quads.push(...makeRDFConstraint(c)) + quads.push(...constraintToQuads(c)) }); return quads } diff --git a/test/integration/ODRL-newTestCases.test.ts b/test/integration/ODRL-newTestCases.test.ts index 29c9b22..a833ebe 100644 --- a/test/integration/ODRL-newTestCases.test.ts +++ b/test/integration/ODRL-newTestCases.test.ts @@ -4,7 +4,7 @@ import { ODRLEvaluator } from "../../src/evaluator/Evaluate"; import { ODRLEngineMultipleSteps } from "../../src/evaluator/Engine"; import { ODRL, REPORT } from "../../src/util/Vocabularies" import { blanknodeify } from "../../src/util/RDFUtil" -import { createPolicy, makeRDFPolicy, Policy } from "../../src/util/policy/PolicyUtil"; +import { createPolicy, policyToQuads, Policy } from "../../src/util/policy/PolicyUtil"; import { makeRDFRequest, Request } from "../../src/util/request/RequestUtil"; import { createRandomUrn } from "../../src/util/Util"; import { write } from "@jeswr/pretty-turtle/dist"; @@ -713,7 +713,7 @@ ex:purpose a dpv:AccountManagement . operator: operator, identifier: constraintID }) - const policyQuads = makeRDFPolicy(policy) + const policyQuads = policyToQuads(policy) request.context.push({ leftOperand: leftOperand, @@ -741,7 +741,7 @@ ex:purpose a dpv:AccountManagement . operator: operator, identifier: constraintID }) - const policyQuads = makeRDFPolicy(policy) + const policyQuads = policyToQuads(policy) request.context.push({ leftOperand: leftOperand, @@ -770,7 +770,7 @@ ex:purpose a dpv:AccountManagement . operator: operator, identifier: constraintID }) - const policyQuads = makeRDFPolicy(policy) + const policyQuads = policyToQuads(policy) request.context.push({ leftOperand: leftOperand, @@ -799,7 +799,7 @@ ex:purpose a dpv:AccountManagement . operator: operator, identifier: constraintID }) - const policyQuads = makeRDFPolicy(policy) + const policyQuads = policyToQuads(policy) request.context.push({ leftOperand: ODRL.deliveryChannel, diff --git a/test/unit/util/policy/PolicyUtil.test.ts b/test/unit/util/policy/PolicyUtil.test.ts new file mode 100644 index 0000000..4f75d72 --- /dev/null +++ b/test/unit/util/policy/PolicyUtil.test.ts @@ -0,0 +1,158 @@ +import "jest-rdf"; +import { Parser, Quad, Writer } from "n3"; +import { createPolicy, policyToQuads, Policy } from "../../../../src/util/policy/PolicyUtil"; +import { blanknodeify } from "../../../../src/util/RDFUtil"; +import { ODRL } from "../../../../src/util/Vocabularies"; + +const parser = new Parser() + +// === Test parameters as full IRIs === +const policyID = "urn:uuid:ce9fc20e-7c79-474e-8afe-7605accccee8"; +const policyType = ODRL.Agreement; + +const permissionID = "urn:uuid:e51a43e4-616f-4f32-906b-2359955228e5"; +const ruleType = ODRL.Permission +const assignee = "http://example.org/alice"; +const resource = "http://example.org/x"; +const action = "http://www.w3.org/ns/odrl/2/read"; + +const constraintID = "urn:uuid:963698fe-3b44-4b88-8527-501b6c5765a6"; +const leftOperand = "http://www.w3.org/ns/odrl/2/purpose"; +const operator = "http://www.w3.org/ns/odrl/2/eq"; +const rightOperand = "https://w3id.org/dpv#AccountManagement"; + +// === RDF string under test === +const policyString = ` +@prefix dct: . +@prefix odrl: . +@prefix sotw: . + +<${policyID}> a <${policyType}> ; + odrl:uid <${policyID}> ; + odrl:permission <${permissionID}> . + +<${permissionID}> a <${ruleType}> ; + odrl:assignee <${assignee}> ; + odrl:action <${action}> ; + odrl:target <${resource}> ; + odrl:constraint <${constraintID}> . + +<${constraintID}> a odrl:Constraint ; + odrl:leftOperand <${leftOperand}> ; + odrl:operator <${operator}> ; + odrl:rightOperand <${rightOperand}> . +`; + +describe('The Policy Util', () => { + let policyRDF: Quad[] + let policy: Policy + + beforeEach(() => { + policyRDF = parser.parse(policyString); + policy = { + identifier: policyID, + type: policyType, + rules: [{ + assignee: assignee, + target: resource, + action: action, + type: ruleType, + identifier: permissionID, + constraints: [ + { + leftOperand: leftOperand, + rightOperand: rightOperand, + operator: operator, + identifier: constraintID + } + ] + }] + } + }) + it('generates policies as expected.', () => { + const generatedpolicy = policyToQuads(policy) + policy.rules[0].action = "test" + const differentpolicy = policyToQuads(policy) + + expect(blanknodeify(generatedpolicy)).toBeRdfIsomorphic(blanknodeify(policyRDF)) + expect(blanknodeify(differentpolicy)).not.toBeRdfIsomorphic(blanknodeify(policyRDF)) + }) + + it('generate identifiers itself.', () => { + const partialPolicy = { + rules: [{ + assignee: assignee, + target: resource, + action: action, + type: ruleType, + constraints: [ + { + leftOperand: leftOperand, + rightOperand: rightOperand, + operator: operator, + } + ] + }] + } + const generatedpolicy = policyToQuads(createPolicy(partialPolicy)) + + expect(blanknodeify(generatedpolicy)).toBeRdfIsomorphic(blanknodeify(policyRDF)) + }) + + it('correctly generates permission rule with temporal constraint.', () => { + const policyString = ` + @prefix odrl: . +@prefix xsd: . + + + a odrl:Agreement ; + odrl:uid ; + odrl:permission . + + + a odrl:Permission ; + odrl:assignee ; + odrl:action odrl:read ; + odrl:target ; + odrl:constraint . + + + a odrl:Constraint ; + odrl:leftOperand odrl:dateTime ; + odrl:operator odrl:gteq ; + odrl:rightOperand "2020-12-05T02:26:20.000Z"^^xsd:dateTime .` + const policyRDF = parser.parse(policyString) + const generatedpolicy = policyToQuads(createPolicy({ + rules: [{ + assignee: 'urn:alice', + target: 'urn:resource', + action: ODRL.read, + type: ODRL.Permission, + constraints: [{ + leftOperand: ODRL.dateTime, + rightOperand: "2020-12-05T02:26:20.000Z", + operator: ODRL.gteq + }] + }] + })) + expect(blanknodeify(generatedpolicy)).toBeRdfIsomorphic(blanknodeify(policyRDF)) + }) + + it('fails for creating policies for which the constraint type is not implemented yet.', () => { + const policy = createPolicy({ + rules: [{ + assignee: 'urn:alice', + target: 'urn:resource', + action: ODRL.read, + type: ODRL.Permission, + constraints: [{ + leftOperand: ODRL.count, + rightOperand: "5", + operator: ODRL.gteq + }] + }] + }) + expect(() => policyToQuads(policy)).toThrow(Error) + }) +}) + diff --git a/test/unit/util/report/ComplianceReportParsing.test.ts b/test/unit/util/report/ComplianceReportParsing.test.ts new file mode 100644 index 0000000..1638e59 --- /dev/null +++ b/test/unit/util/report/ComplianceReportParsing.test.ts @@ -0,0 +1,217 @@ +import { ActivationState, AttemptState, PolicyReport, PremiseReportType, RuleReportType, SatisfactionState } from '../../../../src/util/report/ComplianceReportTypes' +import { parseComplianceReport, parsePremiseReport, parseRuleReport, parseSingleComplianceReport } from '../../../../src/util/report/ComplianceReportParsing' +import { DataFactory, Parser, Store } from 'n3' +import { XSD } from '../../../../src/util/Vocabularies' +const { namedNode, literal } = DataFactory + +const parser = new Parser() +// Turtle string serialization of a compliance report most features that are part of the compliance report model +const stringReport = ` +@prefix odrl: . +@prefix dct: . +@prefix xsd: . +@prefix report: . + + a report:PolicyReport; + dct:created "2024-02-12T11:20:10.999Z"^^xsd:dateTime; + report:policy ; + report:policyRequest ; + report:ruleReport . + a report:PermissionReport; + report:attemptState report:Attempted; + report:rule ; + report:ruleRequest ; + report:premiseReport , , , ; + report:conditionReport ; + report:activationState report:Inactive. + a report:TargetReport; + report:satisfactionState report:Satisfied. + a report:PartyReport; + report:satisfactionState report:Satisfied. + a report:ActionReport; + report:satisfactionState report:Satisfied. + a report:ConstraintReport; + report:constraint ; + report:constraintLeftOperand "2017-02-12T11:20:10.999Z"^^xsd:dateTime; # NOTE: this will be ignored + report:satisfactionState report:Unsatisfied. +` +const reportQuads = parser.parse(stringReport); + +const report: PolicyReport = { + id: namedNode("urn:uuid:e001b361-9f6e-4e05-84bd-510d2870c83d"), + created: literal("2024-02-12T11:20:10.999Z", XSD.terms.dateTime), + request: namedNode("urn:uuid:1bafee59-006c-46a3-810c-5d176b4be364"), + policy: namedNode("urn:uuid:5aa7f98c-65e0-4ff2-9846-40203203a58a"), + ruleReport: [ + { + id: namedNode("urn:uuid:ac38b6ef-fd36-4b6c-ac14-d27e8475f499"), + type: RuleReportType.PermissionReport, + rule: namedNode("urn:uuid:f21be2f2-5efd-46ca-ac4c-0b37d9b9a526"), + activationState: ActivationState.Inactive, + performanceState: undefined, + deonticState: undefined, + attemptState: AttemptState.Attempted, + requestedRule: namedNode("urn:uuid:186be541-5857-4ce3-9f03-1a274f16bf59"), + premiseReport: [ + { + id: namedNode("urn:uuid:9cb53011-6794-4e44-bb4c-b0e585be276b"), + type: PremiseReportType.TargetReport, + premiseReport: [], + satisfactionState: SatisfactionState.Satisfied + }, + { + id: namedNode("urn:uuid:fdea884d-bd8c-499b-89dc-9c7784a7a6f0"), + type: PremiseReportType.PartyReport, + premiseReport: [], + satisfactionState: SatisfactionState.Satisfied + }, + { + id: namedNode("urn:uuid:8eaf15bf-b0d1-4cee-bf0f-ffac9bb3fb14"), + type: PremiseReportType.ActionReport, + premiseReport: [], + satisfactionState: SatisfactionState.Satisfied + }, + { + id: namedNode("urn:uuid:9d1d8c56-e2ac-467f-a368-3913b7aad001"), + type: PremiseReportType.ConstraintReport, + premiseReport: [], + satisfactionState: SatisfactionState.Unsatisfied, + constraint: namedNode("urn:uuid:constraint:86526f9b-57c2-4c94-b079-9762fec562f1") + }, + ], + conditionReport: [namedNode("urn:uuid:6122101e-a4d6-4e1a-9e35-a3ed124a09b8")] + } + ] +} +describe('The Compliance Report Model Parser', () => { + + it('successfully parses single Compliance Reports', () => { + const parsedReport = parseSingleComplianceReport(reportQuads); + + expect(parsedReport).toEqual(report); + }) + + it('throws an error when no compliance report is found.', () => { + expect(() => parseSingleComplianceReport([])).toThrow(); + }) + it('throws an error when no compliance report is found with that given identifier.', () => { + expect(() => parseComplianceReport(namedNode("urn:uuid:policy"), new Store(reportQuads))).toThrow(); + }) + + + describe('its Rule Report Parser', () => { + it('throws an error when the activation state is not one of the allowed options.', () => { + const ruleReportString = ` +@prefix report: . + a report:PermissionReport; + report:rule ; + report:ruleRequest ; + report:activationState .` + + const quads = parser.parse(ruleReportString); + const store = new Store(quads); + expect(() => parseRuleReport(namedNode("urn:uuid:rule"), store)).toThrow(); + }); + + it('throws an error when multiple activation states are present.', () => { + const ruleReportString = ` +@prefix report: . + a report:PermissionReport; + report:rule ; + report:ruleRequest ; + report:activationState report:Inactive, report:Active.` + expect(() => parseRuleReport(namedNode("urn:uuid:ac38b6ef-fd36-4b6c-ac14-d27e8475f499"), new Store(parser.parse(ruleReportString)))).toThrow() + }) + it('throws an error when multiple attempted states are present.', () => { + const ruleReportString = ` +@prefix report: . + a report:PermissionReport; + report:rule ; + report:ruleRequest ; + report:attemptState report:Attempted, report:NotAttempted.` + expect(() => parseRuleReport(namedNode("urn:uuid:rule"), new Store(parser.parse(ruleReportString)))).toThrow() + }) + + it('throws an error when multiple performed states are present.', () => { + const ruleReportString = ` +@prefix report: . + a report:PermissionReport; + report:rule ; + report:ruleRequest ; + report:performanceState report:Performed, report:Unperformed.` + expect(() => parseRuleReport(namedNode("urn:uuid:rule"), new Store(parser.parse(ruleReportString)))).toThrow() + }) + + it('throws an error when multiple deontic states are present.', () => { + const ruleReportString = ` +@prefix report: . + a report:PermissionReport; + report:rule ; + report:ruleRequest ; + report:deonticState report:Fulfilled, report:Violated.` + expect(() => parseRuleReport(namedNode("urn:uuid:rule"), new Store(parser.parse(ruleReportString)))).toThrow() + }) + + }) + describe('its Premise Report Parser', () => { + + it('succesfully parses a recursivily premise reports', () => { + const premiseReportString = ` +@prefix report: . + + a report:ActionReport; + report:satisfactionState report:Satisfied; + report:premiseReport . + a report:ConstraintReport; + report:constraint ; + report:satisfactionState report:Unsatisfied.` + const premiseReportQuads = parser.parse(premiseReportString); + const premiseReport = + { + id: namedNode("urn:uuid:8eaf15bf-b0d1-4cee-bf0f-ffac9bb3fb14"), + type: PremiseReportType.ActionReport, + premiseReport: [ + { + id: namedNode("urn:uuid:9d1d8c56-e2ac-467f-a368-3913b7aad001"), + type: PremiseReportType.ConstraintReport, + premiseReport: [], + satisfactionState: SatisfactionState.Unsatisfied, + constraint: namedNode("urn:uuid:constraint:86526f9b-57c2-4c94-b079-9762fec562f1") + } + ], + satisfactionState: SatisfactionState.Satisfied + } + expect(parsePremiseReport(namedNode("urn:uuid:8eaf15bf-b0d1-4cee-bf0f-ffac9bb3fb14"), new Store(premiseReportQuads))).toEqual(premiseReport) + }) + + it('throws an error when there are circular premise Reports.', () => { + const circularString = ` +@prefix report: . + + a report:ActionReport; + report:satisfactionState report:Satisfied; + report:premiseReport . + + a report:ConstraintReport; + report:satisfactionState report:Unsatisfied; + report:premiseReport .` + const quads = parser.parse(circularString); + + expect(() => parsePremiseReport(namedNode("urn:uuid:A"), new Store(quads))).toThrow() + }) + + + it('throws an error when a constraint report does not have a `report:constraint` property.', () => { + const invalidConstraintString = ` +@prefix report: . + + a report:ConstraintReport; + report:satisfactionState report:Unsatisfied.` + const quads = parser.parse(invalidConstraintString); + + expect(() => parsePremiseReport(namedNode("urn:uuid:C"), new Store(quads))).toThrow() + }) + + }) + +}) \ No newline at end of file diff --git a/test/unit/util/report/ComplianceReportSerialization.test.ts b/test/unit/util/report/ComplianceReportSerialization.test.ts new file mode 100644 index 0000000..125565d --- /dev/null +++ b/test/unit/util/report/ComplianceReportSerialization.test.ts @@ -0,0 +1,96 @@ +import { ActivationState, AttemptState, DeonticState, PerformanceState, PolicyReport, PremiseReportType, RuleReportType, SatisfactionState } from '../../../../src/util/report/ComplianceReportTypes' +import { serializeComplianceReport } from '../../../../src/util/report/ComplianceReportSerialization' +import { DataFactory, Parser, } from 'n3' +import { XSD } from '../../../../src/util/Vocabularies' +const { namedNode, literal } = DataFactory +import "jest-rdf"; +const parser = new Parser() +// Turtle string serialization of a compliance report most features that are part of the compliance report model +const stringReport = ` +@prefix odrl: . +@prefix dct: . +@prefix xsd: . +@prefix report: . + + a report:PolicyReport; + dct:created "2024-02-12T11:20:10.999Z"^^xsd:dateTime; + report:policy ; + report:policyRequest ; + report:ruleReport . + a report:PermissionReport; + report:attemptState report:Attempted; + report:performanceState report:Performed; + report:deonticState report:Violated; + report:rule ; + report:ruleRequest ; + report:premiseReport , , ; + report:conditionReport ; + report:activationState report:Inactive. + a report:TargetReport; + report:satisfactionState report:Satisfied. + a report:PartyReport; + report:satisfactionState report:Satisfied. + a report:ActionReport; + report:satisfactionState report:Satisfied; + report:premiseReport . + a report:ConstraintReport; + report:constraint ; + report:satisfactionState report:Unsatisfied. +` +const reportQuads = parser.parse(stringReport); + +const report: PolicyReport = { + id: namedNode("urn:uuid:e001b361-9f6e-4e05-84bd-510d2870c83d"), + created: literal("2024-02-12T11:20:10.999Z", XSD.terms.dateTime), + request: namedNode("urn:uuid:1bafee59-006c-46a3-810c-5d176b4be364"), + policy: namedNode("urn:uuid:5aa7f98c-65e0-4ff2-9846-40203203a58a"), + ruleReport: [ + { + id: namedNode("urn:uuid:ac38b6ef-fd36-4b6c-ac14-d27e8475f499"), + type: RuleReportType.PermissionReport, + rule: namedNode("urn:uuid:f21be2f2-5efd-46ca-ac4c-0b37d9b9a526"), + activationState: ActivationState.Inactive, + performanceState: PerformanceState.Performed, + deonticState: DeonticState.Violated, + attemptState: AttemptState.Attempted, + requestedRule: namedNode("urn:uuid:186be541-5857-4ce3-9f03-1a274f16bf59"), + premiseReport: [ + { + id: namedNode("urn:uuid:9cb53011-6794-4e44-bb4c-b0e585be276b"), + type: PremiseReportType.TargetReport, + premiseReport: [], + satisfactionState: SatisfactionState.Satisfied + }, + { + id: namedNode("urn:uuid:fdea884d-bd8c-499b-89dc-9c7784a7a6f0"), + type: PremiseReportType.PartyReport, + premiseReport: [], + satisfactionState: SatisfactionState.Satisfied + }, + { + id: namedNode("urn:uuid:8eaf15bf-b0d1-4cee-bf0f-ffac9bb3fb14"), + type: PremiseReportType.ActionReport, + premiseReport: [ + { + id: namedNode("urn:uuid:9d1d8c56-e2ac-467f-a368-3913b7aad001"), + type: PremiseReportType.ConstraintReport, + premiseReport: [], + satisfactionState: SatisfactionState.Unsatisfied, + constraint: namedNode("urn:uuid:constraint:86526f9b-57c2-4c94-b079-9762fec562f1") + } + ], + satisfactionState: SatisfactionState.Satisfied + } + ], + conditionReport: [namedNode("urn:uuid:6122101e-a4d6-4e1a-9e35-a3ed124a09b8")] + } + ] +} +describe('The Compliance Report Model Serializer', () => { + it('successfully serializes Compliance Reports', () => { + const serializedReport = serializeComplianceReport(report) + + expect(serializedReport).toBeRdfIsomorphic(reportQuads); + }) + +}) diff --git a/test/unit/util/request/RequestUtil.test.ts b/test/unit/util/request/RequestUtil.test.ts new file mode 100644 index 0000000..a1f9f26 --- /dev/null +++ b/test/unit/util/request/RequestUtil.test.ts @@ -0,0 +1,76 @@ +import { Parser, Quad } from "n3"; +import { blanknodeify } from "../../../../src"; +import { Request, makeRDFRequest } from "../../../../src/util/request/RequestUtil"; +import "jest-rdf"; + +const parser = new Parser() + +// === Test parameters as full IRIs === +const requestID = "urn:uuid:ce9fc20e-7c79-474e-8afe-7605accccee8"; +const permissionID = "urn:uuid:e51a43e4-616f-4f32-906b-2359955228e5"; +const constraintID = "urn:uuid:963698fe-3b44-4b88-8527-501b6c5765a6"; + +const assignee = "http://example.org/alice"; +const resource = "http://example.org/x"; +const action = "http://www.w3.org/ns/odrl/2/read"; +const leftOperand = "http://www.w3.org/ns/odrl/2/purpose"; +const operator = "http://www.w3.org/ns/odrl/2/eq"; +const rightOperand = "https://w3id.org/dpv#AccountManagement"; + +// === RDF string under test === +const requestString = ` +@prefix dct: . +@prefix odrl: . +@prefix sotw: . + +<${requestID}> a odrl:Request ; + odrl:uid <${requestID}> ; + odrl:permission <${permissionID}> . + +<${permissionID}> a odrl:Permission ; + odrl:assignee <${assignee}> ; + odrl:action <${action}> ; + odrl:target <${resource}> ; + sotw:context <${constraintID}> . + +<${constraintID}> a odrl:Constraint ; + odrl:leftOperand <${leftOperand}> ; + odrl:operator <${operator}> ; + odrl:rightOperand <${rightOperand}> . +`; + +describe('The Request Util', () => { + let requestRDF: Quad[] + let request: Request + + beforeEach(() => { + requestRDF = parser.parse(requestString); + request = { + assignee: assignee, + resource: resource, + action: action, + identifier: requestID, + context: [ + { + leftOperand: leftOperand, + rightOperand: rightOperand, + operator: operator, + identifier: constraintID + } + ] + } + }) + it('generates requests as expected.', () => { + const generatedRequest = makeRDFRequest(request) + request.action = "test" + const differentRequest = makeRDFRequest(request) + + expect(blanknodeify(generatedRequest)).toBeRdfIsomorphic(blanknodeify(requestRDF)) + expect(blanknodeify(differentRequest)).not.toBeRdfIsomorphic(blanknodeify(requestRDF)) + }) + + it('generates requests using the permission identifier.', () => { + const generatedRequest = makeRDFRequest(request, permissionID) + expect(generatedRequest).toBeRdfIsomorphic(requestRDF) + }) +}) \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index c2a8f69..ed5a5c6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,7 @@ "sourceMap": true, "stripInternal": true, "module": "NodeNext", // required for ESM and commonjs library combination: https://stackoverflow.com/questions/71463698/why-we-need-nodenext-typescript-compiler-option-when-we-have-esnext + "isolatedModules": true, // ignores the warning generated by above module }, "include": [ "src" From 3d8947720fe4f1c95b9feb9ddfce2b308b07847d Mon Sep 17 00:00:00 2001 From: woutslabbinck Date: Wed, 21 Jan 2026 16:42:07 +0100 Subject: [PATCH 2/2] docs: add release notes --- CHANGELOG.md | 16 +++++++++++++++- RELEASE_NOTES.md | 14 ++++++++++++++ package-lock.json | 4 ++-- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae2d30c..c2ac852 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,20 @@ # ODRL Evaluator release notes -## [0.5.1](https://github.com/SolidLabResearch/ODRL-Evaluator/compare/v0.5.0...v0.5.2) (2025-12-02) + +## [0.6.0](https://github.com/SolidLabResearch/ODRL-Evaluator/compare/v0.5.1...v0.6.0) (2026-01-21) + + +### Features + +* add eyeling as a reasoner ([9b3ce19](https://github.com/SolidLabResearch/ODRL-Evaluator/commit/9b3ce19a65bdd48d0f434d8d4333488795bc0498)) +* add implementation and test for core parsing/serialization for policy, request and report ([0f812d2](https://github.com/SolidLabResearch/ODRL-Evaluator/commit/0f812d28277d7fa102f791738731f00333a74991)) + + +### Fixes + +* add proper index.core.ts ([d6689f3](https://github.com/SolidLabResearch/ODRL-Evaluator/commit/d6689f34705bb44fdb97f9694f0db780cfc0e5aa)) + +## [0.5.1](https://github.com/SolidLabResearch/ODRL-Evaluator/compare/v0.5.0...v0.5.1) (2025-12-02) ### Features diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index b039f1f..80f30b1 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,19 @@ # ODRL Evaluator +## 0.6.0 + +### New Features + +* Addition of tests for the policy, request and compliance report utils + * `test/unit/util/report` + * `test/unit/util/request` + * `test/unit/util/policy` +* Addition of a new EYE reasoner (`eyeling`) as an alternative for EyeJS + * `src/reasoner/EyelingReasoner.ts` + * Added a demo showcasing how the Reasoner can be used: `demo/test-eyeling.ts` + * Change to the :getUUID builtin `src/rules/built-ins.n3` + * required to support both eyeling and EyeJS + ## 0.5.1 ### New Features diff --git a/package-lock.json b/package-lock.json index 54bcbea..2b716ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "odrl-evaluator", - "version": "0.5.1", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "odrl-evaluator", - "version": "0.5.1", + "version": "0.6.0", "license": "MIT", "dependencies": { "@jeswr/pretty-turtle": "^1.8.2",