Skip to content
Merged
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
16 changes: 15 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
14 changes: 14 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 2 additions & 1 deletion documentation/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
- User-Managed Access Server at https://github.com/SolidLabResearch/user-managed-access
- Policy Conflict Resolver at https://github.com/joachimvh/policy-conflict-resolver
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/evaluator/Atomizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";


Expand Down
3 changes: 2 additions & 1 deletion src/index.core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
1 change: 1 addition & 0 deletions src/util/Vocabularies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createVocabulary } from 'rdf-vocabulary';
export const DC = createVocabulary(
'http://purl.org/dc/terms/',
'created',
'issued',
'title',
'description'
)
Expand Down
6 changes: 3 additions & 3 deletions src/util/policy/PolicyUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand Down Expand Up @@ -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
};
}

/**
Expand All @@ -69,21 +103,87 @@ 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<string> = new Set()
identifier: Quad_Subject,
store: Store,
visited: Set<string> = new Set()
): PremiseReport {
const idStr = (identifier as NamedNode).value;
if (visited.has(idStr)) {
throw new Error(`Circular premise report detected at ${idStr}`);
}
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<T extends Record<string, string>>(
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];
}

78 changes: 78 additions & 0 deletions src/util/report/ComplianceReportSerialization.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading