iface-js
Version:
266 lines (236 loc) • 8.68 kB
JavaScript
class Iface {
constructor() {}
static register(Sig, args) {
let key = args[0]
if (!key)
throw new Error('Missing iface key')
if (!(Sig.prototype instanceof Iface))
throw new Error('Expected Iface subclass')
if (Iface.registered[key])
throw new Error(`Another iface is already registered with key ${key}`)
Iface.registered[key] = [Sig, args]
}
}
Iface.registered = {}
class LiteralIface extends Iface {
constructor(val, eq) {
super()
this.depth = 1
this.val = val
if (eq instanceof Function) {
this.eq = eq
this.match = this.matchCustom
}
}
match(val) { return this.val === val }
matchCustom(val) { return this.eq(this.val, val) }
toString() { return `(LiteralIface: ${this.val})` }
}
class ConstructorIface extends Iface {
constructor(constr) {
if (!(constr instanceof Function))
throw new Error('Function expected for ConstructorIface')
super()
this.constr = constr
this.depth = 1
if (constr === Number) this.match = this.matchNumber
if (constr === String) this.match = this.matchString
}
match(val) {
return val instanceof this.constr
}
matchNumber(val) { return typeof val === 'number'}
matchString(val) { return typeof val === 'string'}
toString() { return `(ConstructorIface: ${this.constr.name || 'Unnamed'})` }
}
class PrototypeIface extends Iface {
constructor(proto) {
if (proto == null || typeof proto !== 'object')
throw new Error('Object expected for PrototypeIface')
super()
this.proto = proto
this.depth = 1
if (proto === Number.prototype) this.match = this.matchNumber
if (proto === String.prototype) this.match = this.matchString
}
match(val) {
return this.proto.isPrototypeOf(val)
}
matchNumber(val) { return typeof val === 'number'}
matchString(val) { return typeof val === 'string'}
toString() {
let contents = Object.keys(this.proto).join(', ')
return `(PrototypeIface: {${contents}})`
}
}
class RecordIface extends Iface {
constructor(spec, strict) {
if (spec == null || typeof spec !== 'object')
throw new Error('Expected object as RecordIface struct')
super()
this.struct = {}
this.keys = []
this.strict = strict
for (let key in spec) {
this.struct[key] = iface(spec[key])
this.keys.push(key)
}
this.depth = 1 + this.keys.reduce((max, key) => Math.max(max, this.struct[key].depth), 0)
}
match(val) {
if (val == null) return false
if (this.strict && Object.keys(val).length !== this.keys.length) return false
for (let i = 0; i < this.keys.length; i++) {
let key = this.keys[i]
if (!this.struct[key].match(val[key])) return false
}
return true
}
toString() {
let contents = this.keys
.map((key) => `${key}: ${this.struct[key]}`)
.join(', ')
return `(RecordIface: {${contents}})`
}
}
class MapIface extends Iface {
constructor(spec) {
super()
this.prop = iface(spec)
this.depth = this.prop.depth + 1
}
match(val) {
if (val == null) return false
for (let key in val) {
if (!this.prop.match(val[key])) return false
}
return true
}
toString() { return `(MapIface: ${this.prop})` }
}
class ArrayIface extends Iface {
constructor(spec) {
super()
this.elem = iface(spec)
this.depth = this.elem.depth + 1
}
match(val) {
if (!(val instanceof Array)) return false
for (let i = 0; i < val.length; i++) {
if (!this.elem.match(val[i])) return false
}
return true
}
toString() { return `(ArrayIface: ${this.elem})` }
}
class TupleIface extends Iface {
constructor(spec, strict=false) {
super()
if (!(spec instanceof Array)) throw new Error('Array of ifaces expected for TupleIface')
this.struct = spec.map((subSpec) => iface(subSpec))
this.length = this.struct.length
this.strict = strict
this.depth = 1 + this.struct.reduce((max, i) => Math.max(max, i.depth), 0)
}
match(val) {
if (!(val instanceof Array)) return false
if (val.length < this.length) return false
if (this.strict && val.length > this.length) return false
for (let i = 0; i < this.length; i++) {
if (!this.struct[i].match(val[i])) return false
}
return true
}
toString() {
let contents = this.struct.join(', ')
return `(TupleIface: [${contents}])`
}
}
class AnyIface extends Iface {
constructor(spec=[]) {
if (!(spec instanceof Array)) throw new Error('Array of ifaces expected for AnyIface')
if (spec instanceof Array && !spec.length && AnyIface.never)
return AnyIface.never
super()
this.struct = spec.map((subSpec) => iface(subSpec))
this.depth = 1 + this.struct.reduce((max, i) => Math.max(max, i.depth), 0)
if (!spec.length && !AnyIface.never) AnyIface.never = this
}
match(val) {
for (let i = 0; i < this.struct.length; i++) {
if (this.struct[i].match(val)) return true
}
return false
}
toString() {
let contents = this.struct.join(', ')
return `(AnyIface: [${contents}])`
}
}
class AllIface extends Iface {
constructor(spec=[]) {
if (!(spec instanceof Array)) throw new Error('Array of ifaces expected for AllIface')
if (spec instanceof Array && !spec.length && AllIface.always)
return AllIface.always
super()
this.struct = spec.map((subSpec) => iface(subSpec))
this.depth = 1 + this.struct.reduce((max, i) => Math.max(max, i.depth), 0)
if (!spec.length && !AllIface.always) AllIface.always = this
}
match(val) {
// console.log(`match: ${val} ${this.toString()}`)
for (let i = 0; i < this.struct.length; i++) {
if (!this.struct[i].match(val)) return false
}
return true
}
toString() {
let contents = this.struct.join(', ')
return `(AllIface: [${contents}])`
}
}
function iface(spec) {
let t = typeof spec
switch (true) {
case spec instanceof Iface: return spec
case t === 'function': return new ConstructorIface(spec)
case !spec || t !== 'object': return new LiteralIface(spec)
case spec instanceof Array: return new ArrayIface(spec[0])
}
for (let key in spec) {
if (Iface.registered[key]) {
let [Sig, args] = Iface.registered[key]
return new Sig(...args.map((arg) => spec[arg]))
}
}
return new RecordIface(spec)
}
Iface.register(LiteralIface, ['$literal', 'eq'])
Iface.register(TupleIface, ['$tuple', 'strict'])
Iface.register(RecordIface, ['$record', 'strict'])
Iface.register(ConstructorIface, ['$constr'])
Iface.register(PrototypeIface, ['$proto'])
Iface.register(MapIface, ['$map'])
Iface.register(ArrayIface, ['$array'])
Iface.register(AnyIface, ['$any'])
Iface.register(AllIface, ['$all'])
iface.Iface = Iface
iface.LiteralIface = LiteralIface
iface.ConstructorIface = ConstructorIface
iface.RecordIface = RecordIface
iface.MapIface = MapIface
iface.ArrayIface = ArrayIface
iface.TupleIface = TupleIface
iface.AnyIface = AnyIface
iface.AllIface = AllIface
iface.literal = function literal(spec, eq) { return new LiteralIface(spec, eq) }
iface.record = function record(spec, strict) { return new RecordIface(spec, strict) }
iface.tuple = function tuple(spec, strict) { return new TupleIface(spec, strict) }
iface.constr = function constr(spec) { return new ConstructorIface(spec) }
iface.proto = function proto(spec) { return new PrototypeIface(spec) }
iface.map = function map(spec) { return new MapIface(spec) }
iface.array = function array(spec) { return new ArrayIface(spec) }
iface.any = function any(spec) { return new AnyIface(spec) }
iface.all = function all(spec) { return new AllIface(spec) }
iface.iface = iface
module.exports = iface