Last active
May 20, 2020 00:14
-
-
Save joeltg/513c60fac8e4c7917990011190aafbff to your computer and use it in GitHub Desktop.
Underlay schema validator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as t from "io-ts/lib/index" | |
import { isRight, Right } from "fp-ts/lib/Either" | |
const rdfType = t.literal("http://www.w3.org/1999/02/22-rdf-syntax-ns#type") | |
type Predicate = t.StringC | typeof rdfType | |
type ShapeExpr = t.Type<shapeExpr> | |
type ValueExpr = ShapeExpr | typeof Singular | |
type Cardinality = t.NumberC | t.LiteralC<1> | |
const ObjectLiteral = t.intersection([ | |
t.type({ value: t.string }), | |
t.partial({ language: t.string, type: t.string }), | |
]) | |
const objectValue = t.union([t.string, ObjectLiteral]) | |
const Annotation = t.type({ | |
type: t.literal("Annotation"), | |
predicate: t.string, | |
object: objectValue, | |
}) | |
const SemAct = t.intersection([ | |
t.type({ type: t.literal("SemAct"), name: t.string }), | |
t.partial({ code: t.string }), | |
]) | |
const Wildcard = t.type({ type: t.literal("Wildcard") }) | |
const IriStem = t.type({ type: t.literal("IriStem"), stem: t.string }) | |
const IriStemRange = t.type({ | |
type: t.literal("IriStemRange"), | |
stem: t.union([t.string, Wildcard]), | |
exclusions: t.array(t.union([t.string, IriStem])), | |
}) | |
const LiteralStem = t.type({ type: t.literal("LiteralStem"), stem: t.string }) | |
const LiteralStemRange = t.type({ | |
type: t.literal("LiteralStemRange"), | |
stem: t.union([t.string, Wildcard]), | |
exclusions: t.array(t.union([t.string, LiteralStem])), | |
}) | |
const Language = t.type({ type: t.literal("Language"), languageTag: t.string }) | |
const LanguageStem = t.type({ type: t.literal("LanguageStem"), stem: t.string }) | |
const LanguageStemRange = t.type({ | |
type: t.literal("LanguageStemRange"), | |
stem: t.union([t.string, Wildcard]), | |
exclusions: t.array(t.union([t.string, LanguageStem])), | |
}) | |
const valueSetValue = t.union([ | |
objectValue, | |
IriStem, | |
IriStemRange, | |
LiteralStem, | |
LiteralStemRange, | |
Language, | |
LanguageStem, | |
LanguageStemRange, | |
]) | |
const nodeKinds = { iri: null, bnode: null, nonliteral: null, literal: null } | |
const NodeConstraint = t.intersection([ | |
t.type({ type: t.literal("NodeConstraint") }), | |
t.partial({ | |
nodeKind: t.keyof(nodeKinds), | |
values: t.array(valueSetValue), | |
datatype: t.string, | |
length: t.number, | |
minlength: t.number, | |
maxlength: t.number, | |
pattern: t.string, | |
flags: t.string, | |
mininclusive: t.number, | |
maxinclusive: t.number, | |
maxexclusive: t.number, | |
totaldigits: t.number, | |
fractiondigits: t.number, | |
}), | |
]) | |
type NodeConstraint = t.TypeOf<typeof NodeConstraint> | |
type tripleConstraints< | |
P extends Predicate, | |
V extends ValueExpr, | |
C extends Cardinality | |
> = [ | |
TripleConstraint<P, V, C>, | |
...TripleConstraint<t.StringC, ShapeExpr, t.NumberC>[] | |
] | |
const uniqueTripleConstraints = < | |
P extends Predicate, | |
V extends ValueExpr, | |
C extends Cardinality | |
>( | |
predicate: P, | |
valueExpr: V, | |
cardinality: C | |
) => | |
new t.Type<tripleConstraints<P, V, C>>( | |
"uniqueTripleConstraints", | |
(input: unknown): input is tripleConstraints<P, V, C> => { | |
if (t.array(TripleConstraint(t.string, shapeExpr, t.number)).is(input)) { | |
if (input.length > 0) { | |
const constraint = TripleConstraint(predicate, valueExpr, cardinality) | |
if (constraint.is(input[0])) { | |
const predicates = new Set(input.map((c) => c.predicate)) | |
return predicates.size === input.length | |
} | |
} | |
} | |
return false | |
}, | |
(input, context) => { | |
const result = t | |
.array(TripleConstraint(t.string, shapeExpr, t.number)) | |
.validate(input, context) | |
if (isRight(result)) { | |
if (result.right.length > 0) { | |
const constraint = TripleConstraint(predicate, valueExpr, cardinality) | |
const first = constraint.validate(result.right[0], context) | |
if (isRight(first)) { | |
const predicates = new Set(result.right.map((c) => c.predicate)) | |
if (predicates.size === result.right.length) { | |
return result as Right<tripleConstraints<P, V, C>> | |
} | |
} else { | |
return first | |
} | |
} | |
return t.failure(input, context) | |
} else { | |
return result | |
} | |
}, | |
t.identity | |
) | |
interface EachOf< | |
P extends Predicate, | |
V extends ValueExpr, | |
C extends Cardinality | |
> { | |
type: "EachOf" | |
expressions: tripleConstraints<P, V, C> | |
id?: string | |
semActs?: t.TypeOf<typeof SemAct>[] | |
annotations?: t.TypeOf<typeof Annotation>[] | |
min?: 1 | |
max?: 1 | |
} | |
const EachOf = < | |
P extends Predicate, | |
V extends ValueExpr, | |
C extends Cardinality | |
>( | |
predicate: P, | |
valueExpr: V, | |
cardinality: C | |
): t.Type<EachOf<P, V, C>> => | |
t.intersection([ | |
t.type({ | |
type: t.literal("EachOf"), | |
expressions: uniqueTripleConstraints(predicate, valueExpr, cardinality), | |
}), | |
t.partial({ | |
id: t.string, | |
semActs: t.array(SemAct), | |
annotations: t.array(Annotation), | |
min: t.literal(1), | |
max: t.literal(1), | |
}), | |
]) | |
interface Shape< | |
P extends Predicate, | |
V extends ValueExpr, | |
C extends Cardinality | |
> { | |
type: "Shape" | |
expression: TripleConstraint<P, V, C> | EachOf<P, V, C> | |
} | |
const Shape = <P extends Predicate, V extends ValueExpr, C extends Cardinality>( | |
predicate: P, | |
valueExpr: V, | |
cardinality: C | |
): t.Type<Shape<P, V, C>> => | |
t.type({ | |
type: t.literal("Shape"), | |
expression: t.union([ | |
TripleConstraint(predicate, valueExpr, cardinality), | |
EachOf(predicate, valueExpr, cardinality), | |
]), | |
}) | |
interface ShapeAnd< | |
P extends Predicate, | |
V extends ValueExpr, | |
C extends Cardinality | |
> { | |
type: "ShapeAnd" | |
shapeExprs: [t.TypeOf<typeof NodeConstraint>, Shape<P, V, C>] | |
} | |
const ShapeAnd = < | |
P extends Predicate, | |
V extends ValueExpr, | |
C extends Cardinality | |
>( | |
predicate: P, | |
valueExpr: V, | |
cardinality: C | |
): t.Type<ShapeAnd<P, V, C>> => | |
t.type({ | |
type: t.literal("ShapeAnd"), | |
shapeExprs: t.tuple([ | |
NodeConstraint, | |
Shape(predicate, valueExpr, cardinality), | |
]), | |
}) | |
const ShapeExternal = t.type({ type: t.literal("ShapeExternal") }) | |
type shapeExprObject< | |
P extends Predicate, | |
V extends ValueExpr, | |
C extends Cardinality | |
> = | |
| Shape<P, V, C> | |
| ShapeOr | |
| ShapeAnd<P, V, C> | |
| t.TypeOf<typeof ShapeExternal> | |
| t.TypeOf<typeof NodeConstraint> | |
interface ShapeOr { | |
type: "ShapeOr" | |
shapeExprs: Array<shapeExpr> | |
} | |
const ShapeOr: t.Type<ShapeOr> = t.recursion("ShapeOr", () => | |
t.type({ | |
type: t.literal("ShapeOr"), | |
shapeExprs: t.array(shapeExpr), | |
}) | |
) | |
const shapeExprObject = < | |
P extends Predicate, | |
V extends ValueExpr, | |
C extends Cardinality | |
>( | |
predicate: P, | |
valueExpr: V, | |
cardinality: C | |
): t.Type<shapeExprObject<P, V, C>> => | |
t.union([ | |
ShapeOr, | |
ShapeAnd(predicate, valueExpr, cardinality), | |
NodeConstraint, | |
Shape(predicate, valueExpr, cardinality), | |
ShapeExternal, | |
]) | |
interface TripleConstraint< | |
P extends Predicate, | |
V extends ValueExpr, | |
C extends Cardinality | |
> { | |
type: "TripleConstraint" | |
predicate: t.TypeOf<P> | |
id?: string | |
valueExpr?: t.TypeOf<V> | |
min?: t.TypeOf<C> | |
max?: t.TypeOf<C> | |
inverse?: false | undefined | |
semActs?: t.TypeOf<typeof SemAct>[] | |
annotations?: t.TypeOf<typeof Annotation>[] | |
} | |
const TripleConstraint = < | |
P extends Predicate, | |
V extends ValueExpr, | |
C extends Cardinality | |
>( | |
predicate: P, | |
valueExpr: V, | |
cardinality: C | |
): t.Type<TripleConstraint<P, V, C>> => | |
t.intersection([ | |
t.type({ | |
type: t.literal("TripleConstraint"), | |
predicate: predicate, | |
}), | |
t.partial({ | |
id: t.string, | |
valueExpr: valueExpr, | |
inverse: t.literal(false), | |
semActs: t.array(SemAct), | |
annotations: t.array(Annotation), | |
min: cardinality, | |
max: cardinality, | |
}), | |
]) | |
type shapeExpr = string | shapeExprObject<t.StringC, ShapeExpr, t.NumberC> | |
const shapeExpr: ShapeExpr = t.recursion("shapeExpr", (shapeExpr) => | |
t.union([t.string, shapeExprObject(t.string, shapeExpr, t.number)]) | |
) | |
interface SingularBrand { | |
readonly Singular: unique symbol | |
} | |
const Singular = t.brand( | |
shapeExpr, | |
(n): n is t.Branded<shapeExpr, SingularBrand> => | |
NodeConstraint.is(n) && | |
Array.isArray(n.values) && | |
n.values.length === 1 && | |
t.string.is(n.values[0]), | |
"Singular" | |
) | |
export const Schema = t.intersection([ | |
t.type({ type: t.literal("Schema") }), | |
t.partial({ | |
shapes: t.array( | |
t.intersection([ | |
shapeExprObject(rdfType, Singular, t.literal(1)), | |
t.type({ id: t.string }), | |
]) | |
), | |
start: shapeExpr, | |
startActs: t.array(SemAct), | |
}), | |
]) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment