Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,45 @@ them:
disagree about what a schema may say, with the evidence for each. Notably,
ShExR admits a `ShapeDecl` as `start` where neither of the others can express
one.

## SPIKE: YAML-LD manifests (branch `manifest-refactor`)

Can the manifests drop their authoritative Turtle -- the SPARQL WG's test-manifest format -- for the YAML-LD manifest format ShEx implementations use for their own examples? Beside each `*/manifest.ttl` this branch has a `manifest.yaml` that says the same thing in that format:

``` yaml
"@context":
- https://www.w3.org/ns/shex-manifest.jsonld
- ../manifest-context.jsonld
comment: "ShEx validation tests"
entries:
## empty {
- name: "0_empty"
status: conformant
trait: [Empty]
comment: "<S1> { } on { }"
approval: Approved
schemaURL: ../schemas/0.shex
shape: http://a.example/S1
dataURL: empty.ttl
node: http://a.example/dummy
```

* Nothing types a test. A validation test is an entry with a `status`, and the status says what `sht:ValidationTest` and `sht:ValidationFailure` said: `conformant` or `nonconformant`. A schema that must be rejected says how (`schemaError: syntax` or `structure`); a representation test names the schema's three serializations (`schemaURL`, `shexjURL`, `shexrURL`).
* An entry is named (`name`), where the Turtle had both `<#name>` and `mf:name`; its inputs are its own, where the Turtle had an `mf:action` node; `approval` is what `mf:status` was, since `status` is the outcome expected.
* `node` and `shape` are IRIs. A focus node that is a blank node or a literal is said as a query map instead -- `queryMap: "_:abcd@<http://a.example/S1>"` -- because that is text: read as JSON-LD, a blank node's label would be lost, and the label is the point of those tests.
* One list replaces the Turtle's two: `entries` holds the tests themselves, in `mf:entries` order, so a test cannot be listed and not defined, or defined and not listed.
* The vocabulary is the [ShEx manifest vocabulary](https://www.w3.org/ns/shex-manifest) (`entries`, `name`, `schema`/`data`/`queryMap` with their `URL` and `Label` spellings, `comment`, and `node`, `shape`, `status` from the ShEx vocabulary), whose source is [`vocab/manifest-vocab.csv`](vocab/manifest-vocab.csv), plus this suite's own terms in [`manifest-context.jsonld`](manifest-context.jsonld). Read as JSON-LD, a manifest is an RDF graph in those vocabularies.

The scripts, with [`bin/manifest-terms.js`](bin/manifest-terms.js) as the one table of which key is which legacy predicate:

* `bin/ttl2yamlld.js manifest.ttl > manifest.yaml` is the migration: it writes the YAML from the Turtle *heuristically* -- from the token stream, not from a graph -- so the Turtle's comments come across, each where it was.
* `bin/yaml2ttl.js manifest.yaml > manifest.ttl` writes the legacy Turtle from the YAML, for the implementations that read it. Its graph is the graph of the Turtle the YAML came from, for all five manifests: nothing was lost.
* `bin/yaml2jsonld.js manifest.yaml > manifest.jsonld` writes the `manifest.jsonld` that `bin/genJSON.js` writes from the Turtle, byte for byte.

shex.js's `packages/shex-manifest/test/TestSuiteManifest-spike-test.js` (its own `manifest-refactor` branch) checks all of that, and `TEST_yaml_manifest=true` drives its validation and parser suites from the YAML. It also checks that these manifests are what they set out to be, the format of shex.js's own examples manifests:

* read as JSON-LD, each is a graph in which nothing it says is dropped;
* `@shexjs/manifest`, the reader for shex.js's examples, reads all five, and what it reads plainly is what a JSON-LD processor reads (`negativeStructure`'s seven `mf:comment` keys, carried across as they were, are the one thing that needs the processor);
* shex.js's examples runner, which knows nothing of this suite, gets the `status` each validation entry states -- for all 1239 of the 1309 that need only what an example does. The other 70 need an import, a semantic-action extension, a query map in a file, or a literal focus or blank-node shape in a query map.

Two things a reader of the graph should know. An entry is identified by its `name`, not by an IRI, so a reference from one test to another (`sameSemanticsAs: "#1dotRefLNex1"`, 23 of them) names an IRI the YAML's graph says nothing else about; `bin/yaml2ttl.js` gives each entry that IRI, `<#name>`, in the Turtle. And the context is published only once this branch is: until then `https://www.w3.org/ns/shex-manifest.jsonld` and `manifest-context.jsonld` are found in the checkouts (shex.js carries a copy of both).
134 changes: 134 additions & 0 deletions bin/manifest-terms.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/* manifest-terms - SPIKE: the correspondence between this suite's YAML-LD
* manifests and the legacy Turtle.
*
* The YAML-LD manifests (manifest.yaml in each suite directory) are written
* in the ShEx manifest vocabulary (https://www.w3.org/ns/shex-manifest, the
* one implementations' own example manifests use) plus this suite's own
* terms (../manifest-context.jsonld). The legacy manifest.ttl is the
* SPARQL WG's test-manifest format: mf:Manifest and mf:entries, each test
* typed sht:ValidationTest, sht:ValidationFailure, ... with its inputs in
* an mf:action node.
*
* bin/ttl2yamlld.js reads this table left to right (to write the YAML from
* the Turtle, once, with its comments); bin/yaml2ttl.js and
* bin/yaml2jsonld.js read it right to left (to write the legacy files from
* the YAML, from now on).
*
* What a row says: `key` in an entry is `legacy` in the Turtle, on the test
* itself (`in: "test"`), in its mf:action node (`"action"`), or in a member
* of its mf:extensionResults list (`"ext"`). `kind` is how the value is
* written:
* iri an IRI reference, as written (relative stays relative)
* term an IRI, a blank node ("_:x"), or a literal ({"@value", "@type"})
* names sht: names without their namespace, as a list
* mfname an mf: name without its namespace
* text a string
* number an integer
* list a collection of nodes, as a list of mappings
* A validation test's node and shape are IRIs. A focus node that is a
* blank node or a literal (or a shape that is a blank node) cannot be: read
* as JSON-LD, a blank node's label is lost and a literal is not a
* reference. Such a test says the same thing as a query map, in ShapeMap
* syntax, which is text: `queryMap: "_:abcd@<http://a.example/S1>"`.
* parseAssociation / writeAssociation go between that and the Turtle's
* sht:focus and sht:shape.
* Several values of one predicate are a list. A reference to another test
* is written as the Turtle wrote it, "#name": an entry is named, and
* <manifest>#<name> is the IRI the generated Turtle gives it.
* `only` restricts a row to entries of that sort when a key serves two
* (schemaURL is an action's sht:schema in a validation test and the test's
* own sx:shex in a schema test).
*/
"use strict";

const PREFIXES = {
rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
rdfs: "http://www.w3.org/2000/01/rdf-schema#",
mf: "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#",
sht: "http://www.w3.org/ns/shacl/test-suite#",
sx: "https://shexspec.github.io/shexTest/ns#",
prov: "http://www.w3.org/ns/prov#",
xsd: "http://www.w3.org/2001/XMLSchema#",
};

const TERMS = [
{key: "trait", legacy: "sht:trait", in: "test", kind: "names"},
{key: "comment", legacy: "rdfs:comment", in: "test", kind: "text"},
{key: "mf:comment", legacy: "mf:comment", in: "test", kind: "text"},
{key: "approval", legacy: "mf:status", in: "test", kind: "mfname"},
{key: "schemaURL", legacy: "sht:schema", in: "action", kind: "iri", only: "validation"},
{key: "shape", legacy: "sht:shape", in: "action", kind: "term"},
{key: "dataURL", legacy: "sht:data", in: "action", kind: "iri"},
{key: "node", legacy: "sht:focus", in: "action", kind: "term"},
{key: "queryMapURL", legacy: "sht:map", in: "action", kind: "iri"},
{key: "semActsURL", legacy: "sht:semActs", in: "action", kind: "iri"},
{key: "shapeExternsURL", legacy: "sht:shapeExterns", in: "action", kind: "iri"},
{key: "resultURL", legacy: "mf:result", in: "test", kind: "iri"},
{key: "extensionResults", legacy: "mf:extensionResults", in: "test", kind: "list"},
{key: "extension", legacy: "mf:extension", in: "ext", kind: "iri"},
{key: "prints", legacy: "mf:prints", in: "ext", kind: "text"},
{key: "wasDerivedFrom", legacy: "prov:wasDerivedFrom", in: "test", kind: "iri"},
{key: "seeAlso", legacy: "rdfs:seeAlso", in: "test", kind: "iri"},
{key: "sameSemanticsAs", legacy: "mf:sameSemanticsAs", in: "test", kind: "iri"},
{key: "schemaURL", legacy: "sx:shex", in: "test", kind: "iri", only: "schema"},
{key: "shexjURL", legacy: "sx:json", in: "test", kind: "iri"},
{key: "shexrURL", legacy: "sx:ttl", in: "test", kind: "iri"},
{key: "startRow", legacy: "mf:startRow", in: "test", kind: "number"},
{key: "startColumn", legacy: "mf:startColumn", in: "test", kind: "number"},
{key: "endRow", legacy: "mf:endRow", in: "test", kind: "number"},
{key: "endColumn", legacy: "mf:endColumn", in: "test", kind: "number"},
];

/** the legacy type of a test, and what says it in the YAML: a validation
* test's expected status; a schema test's expected rejection, or none */
const TYPES = [
{legacy: "sht:ValidationTest", sort: "validation", says: {status: "conformant"}},
{legacy: "sht:ValidationFailure", sort: "validation", says: {status: "nonconformant"}},
{legacy: "sht:NegativeSyntax", sort: "schema", says: {schemaError: "syntax"}},
{legacy: "sht:NegativeStructure", sort: "schema", says: {schemaError: "structure"}},
{legacy: "sht:RepresentationTest", sort: "schema", says: {}},
];

/** an entry's legacy type */
function typeOf (entry) {
if ("status" in entry)
return TYPES.find(t => t.says.status === entry.status);
if ("schemaError" in entry)
return TYPES.find(t => t.says.schemaError === entry.schemaError);
return TYPES.find(t => t.legacy === "sht:RepresentationTest");
}

/** the suite's conventional base: where genJSON.js has always said a
* manifest lives, whatever checkout or branch it was read from */
const baseOf = (dirName) => `https://raw.githubusercontent.com/shexSpec/shexTest/master/${dirName}/manifest`;

/** the YAML's two contexts: the shared vocabulary, then this suite's terms */
const CONTEXTS = ["https://www.w3.org/ns/shex-manifest.jsonld", "../manifest-context.jsonld"];

/** one ShapeMap association as its node and shape: {node, shape}, each
* {iri} | {bnode} | {value, datatype?, language?}; shape null for START */
function parseAssociation (text) {
const m = /^([\s\S]*)@(<[^>]*>|_:[\w.-]+|START)\s*$/.exec(text.trim());
if (!m)
throw new Error("not a single node@shape association: " + JSON.stringify(text));
const term = (t) => {
let l;
if (/^<[^>]*>$/.test(t))
return {iri: t.slice(1, -1)};
if (/^_:[\w.-]+$/.test(t))
return {bnode: t};
if ((l = /^("(?:[^"\\]|\\.)*")(?:\^\^<([^>]*)>|@([A-Za-z]+(?:-[A-Za-z0-9]+)*))?$/.exec(t)))
return Object.assign({value: JSON.parse(l[1])}, l[2] !== undefined ? {datatype: l[2]} : {}, l[3] !== undefined ? {language: l[3]} : {});
throw new Error("not a node: " + JSON.stringify(t));
};
return {node: term(m[1].trim()), shape: m[2] === "START" ? null : term(m[2])};
}

/** ...and back: the association as ShapeMap text */
function writeAssociation ({node, shape}) {
const term = (t) => "iri" in t ? `<${t.iri}>` : "bnode" in t ? t.bnode
: JSON.stringify(t.value) + (t.language ? "@" + t.language : t.datatype ? `^^<${t.datatype}>` : "");
return term(node) + "@" + (shape === null ? "START" : term(shape));
}

module.exports = {PREFIXES, TERMS, TYPES, typeOf, baseOf, CONTEXTS, parseAssociation, writeAssociation};
Loading
Loading