Skip to content

Commit

Permalink
Fix JSON-LD context serialization
Browse files Browse the repository at this point in the history
  • Loading branch information
NoelDeMartin committed May 31, 2024
1 parent 53f74e4 commit 65a2408
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 23 deletions.
65 changes: 43 additions & 22 deletions src/models/internals/JsonLDModelSerializer.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { tap, toString } from '@noeldemartin/utils';
import { arrayFilter, tap, toString } from '@noeldemartin/utils';
import { FieldType, SoukaiError } from 'soukai';
import type { Attributes, BootedArrayFieldDefinition } from 'soukai';
import type { JsonLD } from '@noeldemartin/solid-utils';

import { inferFieldDefinition } from '@/models/fields';
import { isSolidDocumentRelation, isSolidHasRelation } from '@/models/relations/guards';
import type { SolidBootedFieldDefinition } from '@/models/fields';
import type { SolidBootedFieldDefinition, SolidBootedFieldsDefinition } from '@/models/fields';
import type { SolidModel } from '@/models/SolidModel';
import type { SolidRelation } from '@/models/relations/inference';

Expand Down Expand Up @@ -34,20 +34,26 @@ enum IRIFormat {

class JsonLDContext {

public static fromRdfClasses(rdfContexts: Record<string, string>): JsonLDContext {
const terms = Object.entries(rdfContexts).map(([name, value]) => ({
name,
value,
compactingPrefix: `${name}:`,
used: false,
}));

terms.unshift({
public static fromRdfClasses(rdfContexts: Record<string, string>, rdfsProperties: string[]): JsonLDContext {
const [defaultContext, ...otherContexts] = Object.entries(rdfContexts);
const defaultTerm = {
name: '@vocab',
compactingPrefix: '',
value: terms[0]?.value as string,
used: false,
});
value: defaultContext?.[1] as string,
used: true,
};
const terms = otherContexts.reduce((termsIndex, [name, value]) => {
if (value !== defaultTerm.value) {
termsIndex.push({
name,
value,
compactingPrefix: `${name}:`,
used: rdfsProperties.some(rdfProperty => rdfProperty.startsWith(value)),
});
}

return termsIndex;
}, [defaultTerm]);

return new JsonLDContext(terms);
}
Expand All @@ -64,21 +70,28 @@ class JsonLDContext {
this.reverseProperties.set(alias, value);
}

public addTerms(rdfContexts: Record<string, string>): void {
public addTerms(rdfContexts: Record<string, string>, rdfsProperties: string[]): void {
for (const [name, value] of Object.entries(rdfContexts)) {
if (this.terms.some(term => term.value === value))
const existingTerm = this.terms.find(term => term.value === value);

if (existingTerm) {
existingTerm.used ||= rdfsProperties.some(rdfProperty => rdfProperty.startsWith(value));

continue;
}

let termName = name;
let counter = 1;
while (this.terms.some(term => term.name === termName))

while (this.terms.some(term => term.name === termName)) {
termName = `${name}${++counter}`;
}

this.terms.push({
name,
value,
compactingPrefix: `${name}`,
used: false,
used: rdfsProperties.some(rdfProperty => rdfProperty.startsWith(value)),
});
}
}
Expand All @@ -89,8 +102,9 @@ class JsonLDContext {

public render(): Record<string, unknown> {
const rendered = this.terms.reduce((rendered, term) => {
if (term.used)
if (term.used) {
rendered[term.name] = term.value;
}

return rendered;
}, {} as Record<string, unknown>);
Expand All @@ -107,7 +121,9 @@ class JsonLDContext {
export default class JsonLDModelSerializer {

public static forModel(model: typeof SolidModel, compactsIRIs: boolean = true): JsonLDModelSerializer {
const context = JsonLDContext.fromRdfClasses(model.rdfContexts);
const bootedFields = model.fields as SolidBootedFieldsDefinition;
const fieldsRdfProperties = arrayFilter(Object.values(bootedFields).map(definition => definition.rdfProperty));
const context = JsonLDContext.fromRdfClasses(model.rdfContexts, fieldsRdfProperties);

return new JsonLDModelSerializer(context, compactsIRIs ? IRIFormat.Compacted : IRIFormat.Expanded);
}
Expand Down Expand Up @@ -155,7 +171,7 @@ export default class JsonLDModelSerializer {
const term = this.context.getTermForExpandedIRI(expandedIRI);

return term
? tap(term.compactingPrefix + expandedIRI.substr(term.value.length), () => term.used = true)
? tap(term.compactingPrefix + expandedIRI.slice(term.value.length), () => term.used = true)
: expandedIRI;
}

Expand Down Expand Up @@ -195,7 +211,12 @@ export default class JsonLDModelSerializer {
continue;
}

this.context.addTerms(relation.relatedClass.rdfContexts);
const bootedFields = relation.relatedClass.fields as SolidBootedFieldsDefinition;
const fieldsRdfProperties = arrayFilter(
Object.values(bootedFields).map(definition => definition.rdfProperty),
);

this.context.addTerms(relation.relatedClass.rdfContexts, fieldsRdfProperties);
this.setJsonLDRelation(jsonld, relation, ignoredModels);
}
}
Expand Down
20 changes: 20 additions & 0 deletions src/testing/lib/stubs/Task.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { defineSolidModelSchema } from '@/main';
import { FieldType } from 'soukai';

export default defineSolidModelSchema({
rdfsClass: 'schema:Action',
rdfContexts: {
tasks: 'https://vocab.noeldemartin.com/tasks/',
},
fields: {
name: {
type: FieldType.String,
required: true,
},
description: FieldType.String,
important: {
rdfProperty: 'tasks:important',
type: FieldType.Boolean,
},
},
});
5 changes: 5 additions & 0 deletions src/testing/lib/stubs/Task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Model from './Task.schema';

export default class Task extends Model {

}
16 changes: 15 additions & 1 deletion src/tests/soukai-crud.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { faker } from '@noeldemartin/faker';
import type { EngineDocument, InMemoryEngineCollection } from 'soukai';

import Movie from '@/testing/lib/stubs/Movie';
import Task from '@/testing/lib/stubs/Task';
import WatchAction from '@/testing/lib/stubs/WatchAction';

let engine: InMemoryEngine;
Expand All @@ -14,7 +15,7 @@ describe('Soukai CRUD', () => {
engine = new InMemoryEngine;

setEngine(engine);
bootModels({ Movie, WatchAction });
bootModels({ Movie, Task, WatchAction });
});

it('Creates models', async () => {
Expand Down Expand Up @@ -76,6 +77,19 @@ describe('Soukai CRUD', () => {
});
});

it('Adds new fields', async () => {
// Arrange
const task = await Task.create({ name: 'testing' });

// Act
await task.update({ important: true });

// Assert
const freshTask = await task.fresh();

expect(freshTask.important).toBe(true);
});

it('Deletes models', async () => {
// Arrange
const stub = createStub();
Expand Down

0 comments on commit 65a2408

Please sign in to comment.