Skip to content

Instantly share code, notes, and snippets.

@joeltg
Last active May 20, 2020 00:14
Show Gist options
  • Save joeltg/513c60fac8e4c7917990011190aafbff to your computer and use it in GitHub Desktop.
Save joeltg/513c60fac8e4c7917990011190aafbff to your computer and use it in GitHub Desktop.
Underlay schema validator
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