UNPKG

iface-js

Version:
266 lines (236 loc) 8.68 kB
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