semem
Version:
Semantic Memory for Intelligent Agents
276 lines (240 loc) • 8.76 kB
JavaScript
import SPARQLHelpers from '../../utils/SPARQLHelpers.js';
export default class RDFValidator {
constructor(config = {}) {
this.shapes = new Map();
this.constraints = new Map();
this.loadShapes(config.shapes || {});
}
loadShapes(shapes) {
for (const [name, shape] of Object.entries(shapes)) {
this.registerShape(name, shape);
}
}
registerShape(name, shape) {
this.shapes.set(name, {
...shape,
constraints: shape.constraints?.map(c => this.parseConstraint(c)) || []
});
}
parseConstraint(constraint) {
const parsed = {
path: constraint.path,
type: constraint.type,
message: constraint.message
};
switch (constraint.type) {
case 'datatype':
parsed.datatype = constraint.datatype;
break;
case 'pattern':
parsed.pattern = new RegExp(constraint.pattern);
break;
case 'range':
parsed.min = constraint.min;
parsed.max = constraint.max;
break;
case 'cardinality':
parsed.min = constraint.min;
parsed.max = constraint.max;
break;
case 'class':
parsed.class = constraint.class;
break;
case 'in':
parsed.values = new Set(constraint.values);
break;
}
return parsed;
}
generateSHACL(shape) {
const prefixes = {
sh: 'http://www.w3.org/ns/shacl#',
xsd: 'http://www.w3.org/2001/XMLSchema#'
};
let shacl = '';
for (const prefix in prefixes) {
shacl += `@prefix ${prefix}: <${prefixes[prefix]}> .\n`;
}
shacl += `\n${shape.targetClass} a sh:NodeShape ;\n`;
for (const constraint of shape.constraints) {
shacl += this.constraintToSHACL(constraint);
}
return shacl;
}
constraintToSHACL(constraint) {
let shacl = ` sh:property [\n`;
shacl += ` sh:path ${constraint.path} ;\n`;
switch (constraint.type) {
case 'datatype':
shacl += ` sh:datatype ${constraint.datatype} ;\n`;
break;
case 'pattern':
shacl += ` sh:pattern "${constraint.pattern.source}" ;\n`;
break;
case 'range':
if (constraint.min !== undefined) {
shacl += ` sh:minInclusive ${constraint.min} ;\n`;
}
if (constraint.max !== undefined) {
shacl += ` sh:maxInclusive ${constraint.max} ;\n`;
}
break;
case 'cardinality':
if (constraint.min !== undefined) {
shacl += ` sh:minCount ${constraint.min} ;\n`;
}
if (constraint.max !== undefined) {
shacl += ` sh:maxCount ${constraint.max} ;\n`;
}
break;
case 'class':
shacl += ` sh:class ${constraint.class} ;\n`;
break;
case 'in':
shacl += ` sh:in (${Array.from(constraint.values).join(' ')}) ;\n`;
break;
}
if (constraint.message) {
shacl += ` sh:message "${constraint.message}" ;\n`;
}
shacl += ` ] ;\n`;
return shacl;
}
async validate(data, shapeName) {
const shape = this.shapes.get(shapeName);
if (!shape) {
throw new Error(`Shape not found: ${shapeName}`);
}
const validationResults = {
valid: true,
errors: []
};
for (const constraint of shape.constraints) {
try {
const result = await this.validateConstraint(data, constraint);
if (!result.valid) {
validationResults.valid = false;
validationResults.errors.push(result);
}
} catch (error) {
validationResults.valid = false;
validationResults.errors.push({
path: constraint.path,
message: error.message
});
}
}
return validationResults;
}
async validateConstraint(data, constraint) {
const value = this.getValue(data, constraint.path);
switch (constraint.type) {
case 'datatype':
return this.validateDatatype(value, constraint);
case 'pattern':
return this.validatePattern(value, constraint);
case 'range':
return this.validateRange(value, constraint);
case 'cardinality':
return this.validateCardinality(value, constraint);
case 'class':
return this.validateClass(value, constraint);
case 'in':
return this.validateIn(value, constraint);
default:
throw new Error(`Unknown constraint type: ${constraint.type}`);
}
}
getValue(data, path) {
const parts = path.split('.');
let value = data;
for (const part of parts) {
value = value?.[part];
if (value === undefined) break;
}
return value;
}
validateDatatype(value, constraint) {
if (value === undefined) return { valid: true };
const valid = this.checkDatatype(value, constraint.datatype);
return {
valid,
path: constraint.path,
message: valid ? null :
constraint.message || `Invalid datatype: expected ${constraint.datatype}`
};
}
validatePattern(value, constraint) {
if (value === undefined) return { valid: true };
const valid = constraint.pattern.test(String(value));
return {
valid,
path: constraint.path,
message: valid ? null :
constraint.message || `Value does not match pattern: ${constraint.pattern}`
};
}
validateRange(value, constraint) {
if (value === undefined) return { valid: true };
const num = Number(value);
const valid = !isNaN(num) &&
(constraint.min === undefined || num >= constraint.min) &&
(constraint.max === undefined || num <= constraint.max);
return {
valid,
path: constraint.path,
message: valid ? null :
constraint.message || `Value out of range: ${constraint.min} - ${constraint.max}`
};
}
validateCardinality(value, constraint) {
const count = Array.isArray(value) ? value.length : (value === undefined ? 0 : 1);
const valid = (constraint.min === undefined || count >= constraint.min) &&
(constraint.max === undefined || count <= constraint.max);
return {
valid,
path: constraint.path,
message: valid ? null :
constraint.message || `Cardinality violation: expected ${constraint.min}-${constraint.max}`
};
}
validateClass(value, constraint) {
if (value === undefined) return { valid: true };
const valid = value.type === constraint.class;
return {
valid,
path: constraint.path,
message: valid ? null :
constraint.message || `Invalid class: expected ${constraint.class}`
};
}
validateIn(value, constraint) {
if (value === undefined) return { valid: true };
const valid = constraint.values.has(value);
return {
valid,
path: constraint.path,
message: valid ? null :
constraint.message || `Value not in allowed set: ${Array.from(constraint.values).join(', ')}`
};
}
checkDatatype(value, type) {
switch (type) {
case 'xsd:string':
return typeof value === 'string';
case 'xsd:integer':
return Number.isInteger(Number(value));
case 'xsd:decimal':
case 'xsd:float':
case 'xsd:double':
return !isNaN(Number(value));
case 'xsd:boolean':
return typeof value === 'boolean';
case 'xsd:date':
case 'xsd:dateTime':
return !isNaN(Date.parse(value));
default:
return true;
}
}
}