Skip to content

Instantly share code, notes, and snippets.

@tpluscode
Created December 16, 2019 18:38
Show Gist options
  • Save tpluscode/ca40c4e834650b2b70d998f3fb940adf to your computer and use it in GitHub Desktop.
Save tpluscode/ca40c4e834650b2b70d998f3fb940adf to your computer and use it in GitHub Desktop.
Typed clownface
// WARNING
//
// Ugly implementation and not exactly like in the examples. but similar
import { Literal, NamedNode } from 'rdf-js'
import Clownface from 'clownface/lib/Clownface'
import rdf from 'rdf-ext'
import { TypedClownfaceEntity } from './TypedClownfaceEntity'
const trueLiteral: Literal = rdf.literal(true)
type PropRef = string | NamedNode
type TypedEntityConstructor = new (...args: any[]) => TypedClownfaceEntity
interface AccessorOptions {
path?: PropRef | PropRef[];
as?: 'term' | TypedEntityConstructor;
array?: boolean;
}
interface LiteralAccessorOption extends AccessorOptions {
type?: typeof Boolean;
}
function predicate (cf: Clownface, termOrString: PropRef) {
if (typeof termOrString === 'string') {
return cf.namedNode(termOrString)
} else {
return cf.node(termOrString)
}
}
function getNode (cf: Clownface, path: Clownface[]) {
return path
.reduce((node, prop) => {
return node.out(prop)
}, cf)
}
function getPredicate (cf: any, name: PropertyKey): NamedNode {
return (cf.constructor).__ns[name.toString()]
}
function getPath (protoOrDescriptor: any, cf: Clownface, path: PropRef | PropRef[], name: PropertyKey) {
return (path ? Array.isArray(path) ? path : [ path ] : [ getPredicate(protoOrDescriptor, name) ])
.map(termOrString => predicate(cf, termOrString))
}
export function property (options: AccessorOptions = {}) {
const Type = options.as || 'term'
return (protoOrDescriptor: any, name?: PropertyKey): any => {
Object.defineProperty(protoOrDescriptor, name, {
get (this: Clownface): any {
const node = getNode(this, getPath(protoOrDescriptor, this, options.path, name))
const values = node.map(term => {
if (Type === 'term') {
return term.term
}
return new Type(term)
})
if (options.array === true) {
return values
}
if (values.length > 1) {
throw new Error(`Multiple terms found where 0..1 was expected`)
}
return values[0]
},
set (this: Clownface, value: any) {
const path = getPath(protoOrDescriptor, this, options.path, name)
let node: Clownface
node = path.length === 1 ? this : getNode(this, path.slice(path.length - 1))
const lastPredicate = path[path.length - 1]
node.deleteOut(lastPredicate)
.addOut(lastPredicate, value)
},
})
}
}
export function literal (options: LiteralAccessorOption = {}) {
return (protoOrDescriptor: any, name?: PropertyKey): any => {
Object.defineProperty(protoOrDescriptor, name, {
get (this: Clownface): any {
const node = getNode(this, getPath(protoOrDescriptor, this, options.path, name))
if (options.type === Boolean) {
return trueLiteral.equals(node.term)
}
return node.value
},
set (this: Clownface, value: any) {
const path = getPath(protoOrDescriptor, this, options.path, name)
let node: Clownface
node = path.length === 1 ? this : getNode(this, path.slice(path.length - 1))
const lastPredicate = path[path.length - 1]
node.deleteOut(lastPredicate)
.addOut(lastPredicate, value)
},
})
}
}
export function namespace (ns: any) {
return (classOrDescriptor: any) => {
classOrDescriptor.__ns = ns
}
}
import Clownface from 'clownface/lib/Clownface'
import { namespace, literal, property } from './typedClownface'
import ns from '@rdfjs/namespace'
class Table extends Clownface {
// path - one or more predicates to traverse
// as - constructor to wrap the node with (another subclass of Clownface)
// array - how to handle multiple objects
//
// equivalent to
// this.out(dataCube.source)
// .out(dataCube.column)
// .map(cf => new Column(cf))
@property({ path: [ dataCube.source, dataCube.column ], as: Column, array: true })
public readonly columns: Column[]
}
// class decorator which acts like JSON-LD @base
@namespace(ns('http://schema.org/'))
class Column extends Clownface {
// without path param, uses JS prop name for predicate
// equivalent to
// this.out(this.namedNode('http://schema.org/name')).value
@literal()
public name: string
// type param defines built-in cast of the literal value
@literal({ path: csvw.suppressOutput, type: Boolean })
public suppressed: boolean
}
import $rdf = require('rdf-ext')
import clownface = require('clownface')
import { Table } from './types'
import ns from '@rdfjs/namespace'
import { prefixes } from '@zazuko/rdf-vocabularies'
const rdf = ns(prefixes.rdf)
const table = new Table({ dataset: $rdf.dataset(), term: $rdf.namedNode('http://example.com/table' })
// an entity is still a clownface object
const types = table.out(rdf.type).terms
// access columns like they were native objects
table.columns.forEach((column, i) => {
// setters also work
column.name = `Column ${i}`
column.suppressed = true
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment