tpluscode
12/16/2019 - 6:38 PM

Typed clownface

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
})