Skip to content

Commit 412b346

Browse files
Feat/parse and serialize core (#14)
* feat: add implementation and test for core parsing/serialization for policy, request and report * docs: add release notes
1 parent 3b539a9 commit 412b346

19 files changed

Lines changed: 810 additions & 31 deletions

CHANGELOG.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
# ODRL Evaluator release notes
22

3-
## [0.5.1](https://github.com/SolidLabResearch/ODRL-Evaluator/compare/v0.5.0...v0.5.2) (2025-12-02)
3+
4+
## [0.6.0](https://github.com/SolidLabResearch/ODRL-Evaluator/compare/v0.5.1...v0.6.0) (2026-01-21)
5+
6+
7+
### Features
8+
9+
* add eyeling as a reasoner ([9b3ce19](https://github.com/SolidLabResearch/ODRL-Evaluator/commit/9b3ce19a65bdd48d0f434d8d4333488795bc0498))
10+
* add implementation and test for core parsing/serialization for policy, request and report ([0f812d2](https://github.com/SolidLabResearch/ODRL-Evaluator/commit/0f812d28277d7fa102f791738731f00333a74991))
11+
12+
13+
### Fixes
14+
15+
* add proper index.core.ts ([d6689f3](https://github.com/SolidLabResearch/ODRL-Evaluator/commit/d6689f34705bb44fdb97f9694f0db780cfc0e5aa))
16+
17+
## [0.5.1](https://github.com/SolidLabResearch/ODRL-Evaluator/compare/v0.5.0...v0.5.1) (2025-12-02)
418

519

620
### Features

RELEASE_NOTES.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# ODRL Evaluator
22

3+
## 0.6.0
4+
5+
### New Features
6+
7+
* Addition of tests for the policy, request and compliance report utils
8+
* `test/unit/util/report`
9+
* `test/unit/util/request`
10+
* `test/unit/util/policy`
11+
* Addition of a new EYE reasoner (`eyeling`) as an alternative for EyeJS
12+
* `src/reasoner/EyelingReasoner.ts`
13+
* Added a demo showcasing how the Reasoner can be used: `demo/test-eyeling.ts`
14+
* Change to the :getUUID builtin `src/rules/built-ins.n3`
15+
* required to support both eyeling and EyeJS
16+
317
## 0.5.1
418

519
### New Features

documentation/release.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ Steps to follow:
1212
- potentially upgrade dependent repositories
1313
- ODRL Test Suite at https://github.com/SolidLabResearch/ODRL-Test-Suite
1414
- FORCE demo at https://github.com/woutslabbinck/ODRL-Evaluator-Demo
15-
- User-Access Server at https://github.com/SolidLabResearch/user-managed-access
15+
- User-Managed Access Server at https://github.com/SolidLabResearch/user-managed-access
16+
- Policy Conflict Resolver at https://github.com/joachimvh/policy-conflict-resolver

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "odrl-evaluator",
3-
"version": "0.5.1",
3+
"version": "0.6.0",
44
"description": "An open implementation of an ODRL Evaluator that evaluates ODRL policies by generating Compliance Reports",
55
"main": "./dist/index.js",
66
"types": "./dist/index.d.ts",

src/evaluator/Atomizer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const { namedNode, quad } = DataFactory
77

88
import { PolicyAtomizer } from "odrl-atomizer";
99
import { ActivationState, PolicyReport, SatisfactionState } from "../util/report/ComplianceReportTypes";
10-
import { parseComplianceReport } from "../util/report/ComplianceReportUtil";
10+
import { parseComplianceReport } from "../util/report/ComplianceReportParsing";
1111
import { replaceSubject } from "../util/RDFUtil";
1212

1313

src/index.core.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export * from './util/Vocabularies'
1717
export * from './util/Prefixes'
1818

1919
export * from './util/report/ComplianceReportTypes'
20-
export * from './util/report/ComplianceReportUtil'
20+
export * from './util/report/ComplianceReportSerialization'
21+
export * from './util/report/ComplianceReportParsing'
2122
export * from './util/policy/PolicyUtil'
2223
export * from './util/request/RequestUtil'

src/util/Vocabularies.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createVocabulary } from 'rdf-vocabulary';
33
export const DC = createVocabulary(
44
'http://purl.org/dc/terms/',
55
'created',
6+
'issued',
67
'title',
78
'description'
89
)

src/util/policy/PolicyUtil.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export function createPolicy(input: {
120120
/**
121121
* Serialize a Policy into RDF quads.
122122
*/
123-
export function makeRDFPolicy(policy: Policy): Quad[] {
123+
export function policyToQuads(policy: Policy): Quad[] {
124124
const quads: Quad[] = [];
125125
const policyNode = namedNode(policy.identifier);
126126

@@ -166,14 +166,14 @@ export function makeRDFPolicy(policy: Policy): Quad[] {
166166
const constraintNode = namedNode(c.identifier);
167167
quads.push(quad(ruleNode, ODRL.terms.constraint, constraintNode));
168168

169-
quads.push(...makeRDFConstraint(c))
169+
quads.push(...constraintToQuads(c))
170170
});
171171
});
172172

173173
return quads;
174174
}
175175

176-
export function makeRDFConstraint(constraint: Constraint) {
176+
export function constraintToQuads(constraint: Constraint) {
177177
const quads: Quad[] = []
178178
const constraintNode = namedNode(constraint.identifier);
179179
quads.push(quad(constraintNode, RDF.terms.type, ODRL.terms.Constraint));

src/util/report/ComplianceReportUtil.ts renamed to src/util/report/ComplianceReportParsing.ts

Lines changed: 111 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Literal, NamedNode, Quad_Subject, Store } from 'n3';
22
import { DC, RDF, REPORT } from '../Vocabularies';
3-
import { ActivationState, PolicyReport, PremiseReport, PremiseReportType, RuleReport, RuleReportType, SatisfactionState } from './ComplianceReportTypes';
3+
import { ActivationState, AttemptState, DeonticState, PerformanceState, PolicyReport, PremiseReport, PremiseReportType, RuleReport, RuleReportType, SatisfactionState } from './ComplianceReportTypes';
44
import { Quad } from '@rdfjs/types';
55

66
/**
@@ -47,14 +47,48 @@ export function parseComplianceReport(identifier: Quad_Subject, store: Store): P
4747
*/
4848
export function parseRuleReport(identifier: Quad_Subject, store: Store): RuleReport {
4949
const premiseNodes = store.getObjects(identifier, REPORT.premiseReport, null) as NamedNode[];
50+
const activationState = parseSingleState(
51+
store,
52+
identifier,
53+
REPORT.activationState,
54+
ActivationState,
55+
);
56+
57+
const attemptState = parseSingleState(
58+
store,
59+
identifier,
60+
REPORT.attemptState,
61+
AttemptState,
62+
);
63+
64+
const performanceState = parseSingleState(
65+
store,
66+
identifier,
67+
REPORT.performanceState,
68+
PerformanceState,
69+
);
70+
71+
const deonticState = parseSingleState(
72+
store,
73+
identifier,
74+
REPORT.deonticState,
75+
DeonticState,
76+
);
77+
78+
5079
return {
5180
id: identifier as NamedNode,
5281
type: store.getObjects(identifier, RDF.type, null)[0].value as RuleReportType,
53-
activationState: store.getObjects(identifier, REPORT.activationState, null)[0].value as ActivationState,
5482
requestedRule: store.getObjects(identifier, REPORT.ruleRequest, null)[0] as NamedNode,
5583
rule: store.getObjects(identifier, REPORT.rule, null)[0] as NamedNode,
56-
premiseReport: premiseNodes.map((prem) => parsePremiseReport(prem, store))
57-
}
84+
premiseReport: premiseNodes.map((prem) => parsePremiseReport(prem, store)),
85+
conditionReport: store.getObjects(identifier, REPORT.conditionReport, null) as NamedNode[],
86+
87+
activationState,
88+
attemptState,
89+
performanceState,
90+
deonticState
91+
};
5892
}
5993

6094
/**
@@ -69,21 +103,87 @@ export function parseRuleReport(identifier: Quad_Subject, store: Store): RuleRep
69103
* @throws Error if a circular premise report is detected.
70104
*/
71105
export function parsePremiseReport(
72-
identifier: Quad_Subject,
73-
store: Store,
74-
visited: Set<string> = new Set()
106+
identifier: Quad_Subject,
107+
store: Store,
108+
visited: Set<string> = new Set()
75109
): PremiseReport {
76110
const idStr = (identifier as NamedNode).value;
77111
if (visited.has(idStr)) {
78112
throw new Error(`Circular premise report detected at ${idStr}`);
79113
}
80114
visited.add(idStr);
81115

82-
const nestedPremises = store.getObjects(identifier, REPORT.PremiseReport, null) as NamedNode[];
83-
return {
116+
const nestedPremises = store.getObjects(identifier, REPORT.premiseReport, null) as NamedNode[];
117+
118+
let premiseReport: PremiseReport = {
84119
id: identifier as NamedNode,
85120
type: store.getObjects(identifier, RDF.type, null)[0].value as PremiseReportType,
86-
premiseReport: nestedPremises.map((prem) => parsePremiseReport(prem, store)),
87-
satisfactionState: store.getObjects(identifier, REPORT.satisfactionState, null)[0].value as SatisfactionState
121+
premiseReport: nestedPremises.map((prem) => parsePremiseReport(prem, store, visited)),
122+
satisfactionState: parseSingleState(store, identifier, REPORT.satisfactionState, SatisfactionState)
123+
}
124+
125+
if (premiseReport.type === PremiseReportType.ConstraintReport) {
126+
if (store.getObjects(identifier, REPORT.constraint, null).length !== 1) throw new Error("Expected one constraint");
127+
const constraintID = store.getObjects(identifier, REPORT.constraint, null)[0] as NamedNode;
128+
premiseReport.constraint = constraintID
88129
}
130+
return premiseReport
131+
}
132+
133+
/**
134+
* Extracts exactly one state value from an N3 store and validates it
135+
* against a provided enum of allowed values.
136+
*
137+
* This helper enforces three guarantees:
138+
* 1. At most one value exists for the given subject–predicate pair.
139+
* 2. If a value exists, it must match one of the allowed enum values.
140+
* 3. Returns `undefined` when no value is present.
141+
*
142+
* The human‑readable label used in error messages is automatically derived
143+
* from the predicate URI by taking the last path or fragment segment.
144+
*
145+
* @template T - An enum-like object whose values represent valid states.
146+
*
147+
* @param store - The N3 store containing RDF quads.
148+
* @param subject - The subject node from which the state is extracted.
149+
* @param predicate - The predicate identifying the state property (as a URI string).
150+
* @param allowed - The enum of allowed values for this state.
151+
*
152+
* @returns The validated state value, or `undefined` if no value is present.
153+
*
154+
* @throws Error
155+
* Thrown when:
156+
* - More than one value is found for the given predicate.
157+
* - The found value does not match any allowed enum value.
158+
*/
159+
function parseSingleState<T extends Record<string, string>>(
160+
store: Store,
161+
subject: NamedNode | Quad_Subject,
162+
predicate: string,
163+
allowed: T
164+
): T[keyof T] | undefined {
165+
// Derive label from last path or fragment segment
166+
const label =
167+
predicate.split('#').pop()?.split('/').pop() ??
168+
predicate;
169+
170+
const objects = store.getObjects(subject, predicate, null);
171+
172+
if (objects.length > 1) {
173+
throw new Error(`Multiple ${label} values found for ${subject.value}`);
174+
}
175+
176+
if (objects.length === 0) {
177+
return undefined;
178+
}
179+
180+
const value = objects[0].value;
181+
const allowedValues = Object.values(allowed);
182+
183+
if (!allowedValues.includes(value)) {
184+
throw new Error(`Invalid ${label} value for ${subject.value}: ${value}`);
185+
}
186+
187+
return value as T[keyof T];
89188
}
189+

0 commit comments

Comments
 (0)