Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental note body serialization #1

Closed
wants to merge 13 commits into from
60 changes: 60 additions & 0 deletions package-lock.json

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

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
}
],
"devDependencies": {
"@ontologies/core": "^2.0.3",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why dev deps for this one but not the others?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oo I think this is a good catch, thx!

"@size-limit/preset-small-lib": "^7.0.8",
"husky": "^7.0.4",
"size-limit": "^7.0.8",
Expand All @@ -53,6 +54,8 @@
},
"dependencies": {
"@inrupt/solid-client": "^1.21.0",
"@ontologies/rdf": "^2.0.0",
"@ontologies/schema": "^2.0.0",
"@rdf-esm/namespace": "^0.5.5",
"@scure/base": "^1.0.0",
"@types/react": "^18.0.5",
Expand Down
35 changes: 35 additions & 0 deletions src/collections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@

import {
SolidDataset, Thing,
getThing, buildThing, createThing, getUrl,
} from '@inrupt/solid-client';
import { first, rest, nil } from '@ontologies/rdf'

type JSOToThingConverter = (obj: any, path: number[]) => Thing[]

export function arrayToThings(slateArray: object[], jsoToThing: JSOToThingConverter, path: number[] = [0]): Thing[] {
const [el, ...restOfEls] = slateArray
const last = path[path.length - 1]
const [listElementThing, ...listElementSubThings] = jsoToThing(el, path)
const [nextListThing, ...nextListSubThings] = (restOfEls.length > 0) ? arrayToThings(restOfEls, jsoToThing, [...path.slice(0, path.length - 1), last + 1]) : []
const listThing = buildThing(createThing({ name: `li-${path.join("-")}` }))
.addUrl(first.value, listElementThing)
.addUrl(rest.value, nextListThing || nil.value)
.build()

return [listThing, listElementThing, ...listElementSubThings, nextListThing, ...nextListSubThings]
}

export type ThingToJSOConvertor = (thing: Thing, dataset: SolidDataset) => any

export function thingsToArray(thing: Thing, dataset: SolidDataset, thingToSlateObject: ThingToJSOConvertor): object[] {
const restValue = getUrl(thing, rest.value)
const restThing = (restValue && (restValue !== nil.value)) && getThing(dataset, restValue)
const firstUrl = getUrl(thing, first.value)
const firstThing = firstUrl && getThing(dataset, firstUrl)
const firstElement = firstThing && thingToSlateObject(firstThing, dataset)
return firstElement ? [
firstElement,
...(restThing ? thingsToArray(restThing, dataset, thingToSlateObject) : [])
] : []
}
5 changes: 3 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
export * from "./hooks"
export * from "./collections"
export * from "./garden"
export * from "./hooks"
export * from "./items"
export * from "./spaces"
export * from "./settings"
export * from "./spaces"
export * from "./types"
export * from "./utils"
export * from "./vocab"
97 changes: 97 additions & 0 deletions src/note.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import {
SolidDataset, Thing,
getThing, createThing,
addUrl,
setInteger, setDecimal, setStringNoLocale, setBoolean,
getStringNoLocale, getInteger, getBoolean, getUrl,
} from '@inrupt/solid-client';
import { createNS } from "@ontologies/core"

import {arrayToThings, thingsToArray} from "./collections"

const noteNSUrl = "https://mysilio.garden/ontologies/note#"
const noteNS = createNS(noteNSUrl)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use @rdf-esm/namespace for this:
import namespace from '@rdf-esm/namespace

See src/vocab for example usage.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't matter much which one we use, I don't think, because they both build NamedNodes. I think I even came across the @ontologies one, might be by the same guy?


function addKeyValToThing(thing: Thing, key: string, value: any, path: number[]): Thing[] {
if (Array.isArray(value)) {
const [arrayThing, ...restOfThings] = arrayToThings(value, createThingFromSlateJSOElement, [...path, 0])
if (arrayThing) {
return [
addUrl(thing, key, arrayThing),
arrayThing,
...restOfThings
]
} else {
// TODO: is this the right thing to do? arrayThing should never be falsy so maybe throw an error?
return []
}
} else {

switch (typeof value) {
case 'string':
return [setStringNoLocale(thing, key, value)]
case 'boolean':
return [setBoolean(thing, key, value)]
case 'number':
if (Number.isInteger(value)) {
return [setInteger(thing, key, value)]
} else {
return [setDecimal(thing, key, value)]
}
default:
return [thing]
}
}

}

export function createThingFromSlateJSOElement(o: any, path: number[]): Thing[] {
const thing = createThing({ name: (o.id ? `${o.id}` : `el-${path.join("-")}`) })
const otherThings = Object.keys(o).reduce((m: Thing[], k: string) => {
const [mThing, ...mRest] = m;
const [thing, ...restOfThings] = addKeyValToThing(mThing, noteNS(k).value, o[k], path)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like all the places you use this in the downstream function are Inrupt fns. Those all actually take NamedNodes as well as URLStrings for the predicates, so you don't have to do noteNS(k).value here, you could just do noteNS(k) if you wanted.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm, I tried this and got:
Screen Shot 2022-06-29 at 12 13 15 PM

looks like addUrl doesn't accept a NamedNode?

return [thing, ...mRest, ...restOfThings]
}, [thing])
return [thing, ...otherThings]
}

function childrenArrayFromDataset(dataset: SolidDataset, childrenUrl: string) {
const thing = getThing(dataset, childrenUrl);

if (thing) {
return thingsToArray(thing, dataset, noteThingToSlateObject)
} else {
return []
}
}

const childrenPred = noteNS('children')
export function noteThingToSlateObject(thing: Thing, dataset: SolidDataset) {
const obj: any = {}
for (const pred in thing.predicates) {
const [, key] = pred.split(noteNSUrl)
if (key) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noticed the if (key) -- when would key not exist? If the predicate isn't in our NS? That might explain why you match on key below instead of the predicate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh yep! this is expecting that there might be other predicates on the thing and trying to only deal with things in our NS. I'm really not sure about this particular part of the approach - at the very least I need to double check that my namespaces make sense before shipping this, will take another look at this tomorrow!

if (key === 'children') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not instead test to see if pred === childrenPred.value or pred === noteNS('children').value?

To me that reads a little cleaner than splitting on the predicate namespace.

const children = getUrl(thing, childrenPred.value)
if (children) obj.children = childrenArrayFromDataset(dataset, children)
} else {
const literals = thing.predicates[pred].literals
// currently just uses the first literal it can find
if (literals) {
if (literals['http://www.w3.org/2001/XMLSchema#string']) {
const str = getStringNoLocale(thing, pred)
if (str || (str === "")) obj[key] = str
} else if (literals['http://www.w3.org/2001/XMLSchema#boolean']){
const bool = getBoolean(thing, pred)
if (bool === true || bool === false) obj[key] = bool
} else if (literals['http://www.w3.org/2001/XMLSchema#integer']){
const n = getInteger(thing, pred)
if (n) obj[key] = n
}
}
}
}
}

return obj
}
3 changes: 1 addition & 2 deletions test/blah.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { sum } from '../src';

describe('blah', () => {
it('works', () => {
expect(sum(1, 1)).toEqual(2);
expect(1 + 1).toEqual(2);
});
});
33 changes: 33 additions & 0 deletions test/note.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
Thing,
getThing, setThing, createSolidDataset, saveSolidDatasetAt
} from '@inrupt/solid-client';

import { arrayToThings, thingsToArray } from '../src/collections'
import { createThingFromSlateJSOElement, noteThingToSlateObject } from '../src/note'

const noteBody = [{ "children": [{ "text": "Hi everybody!" }], "type": "h1" }, { "type": "p", "id": 1651696062634, "children": [{ "text": "" }] }, { "children": [{ "text": "" }], "type": "h1" }, { "children": [{ "text": "thanks", "bold": true }, { "text": " for coming to the " }, { "text": "digital gardening party", "italic": true }, { "text": "." }], "type": "h2" }, { "children": [{ "text": "" }], "type": "h2" }, { "children": [{ "text": "I'm " }, { "type": "concept", "children": [{ "text": "[[Travis]]" }], "name": "Travis" }, { "text": "." }], "type": "h2" }, { "children": [{ "text": "" }], "type": "h2" }, { "children": [{ "text": "This is not a cat:" }], "type": "h2" }, { "children": [{ "text": "" }], "type": "h2" }, { "children": [{ "text": "" }], "type": "h2" }, { "type": "img", "children": [{ "text": "" }], "url": "https://travis.mysilio.me/public/itme/online/images/1cd18da0-6c9e-11eb-b09e-dd1bdda70e9f.jpg", "originalUrl": "https://travis.mysilio.me/public/itme/online/images/1cd18da0-6c9e-11eb-b09e-dd1bdda70e9f.original.jpg", "alt": "", "mime": "image/jpeg" }]

describe('a JSON note body', () => {
it('can be serialized and deserialized to and from RDF', async () => {
const things: Thing[] = arrayToThings(noteBody, createThingFromSlateJSOElement)
const listUrl = things[0].url

let dataset = things.filter(x => !!x).reduce(setThing, createSolidDataset())

// need to save this to resolve the various local references in Thing objects
dataset = await saveSolidDatasetAt("https://example.com/note", dataset, {
fetch: jest.fn(() =>
Promise.resolve({
ok: true,
url: "https://example.com/note",
headers: new Headers(),
}),
) as jest.Mock
})

const listThing = getThing(dataset, listUrl)
const newNoteBody = listThing && thingsToArray(listThing, dataset, noteThingToSlateObject)
expect(newNoteBody).toEqual(noteBody);
});
});