Skip to content

Commit

Permalink
Change XSLT JSON Schema gen to allow $schema directive (#384)
Browse files Browse the repository at this point in the history
* Allow $schema in XSLT-generated JSON schemas

For #383, allow $schema so JSON schemas that are
emitted by the XSLT-M4 implementation allow downstream consumers to
optionally bind schema references into a document instances and not
immediately invalidate the schema.

* Remove maxProperties for #385.

* Update pre-existing and new tests for #383.

Do not just compare the XML-based syntax tree of whole JSON schemas.

* Update .gitignore per @wendellpiez feedback.

* Add code comment per @wendellpiez feedback.

Explain why we test with contains() and not matching the full field name in the updated
XSpec tests and try to reduce flakiness.
  • Loading branch information
aj-stein-nist authored Jun 27, 2023
1 parent 13cde79 commit a36f579
Show file tree
Hide file tree
Showing 13 changed files with 225 additions and 11 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ node_modules/
# Ignore go files
/build/vendor/

# Ignore XSpec reports and temp directories
xspec/
*report.html
41 changes: 33 additions & 8 deletions test-suite/metaschema-xspec/json-schema-gen/json-schema-gen.xspec
Original file line number Diff line number Diff line change
Expand Up @@ -8,44 +8,69 @@
<x:scenario label="with no constraint and no allowed-values">
<x:context href="json-value_flag_unconstrained_metaschema.xml"/>
<x:expect label="the resulting JSON Schema should not have an enum to enforce it."
test="$x:result => j:json-to-xml()" select="'../json-value_flag_unconstrained.json' => j:unparsed-text() => j:json-to-xml()"/>
test="j:json-to-xml($x:result)//j:map[@key='test-flag']"
select="j:json-to-xml(j:unparsed-text('../json-value_flag_unconstrained.json'))//j:map[@key='test-flag']"/>
</x:scenario>
<x:scenario label="with a constraint, allowed-values, strict enforcement of allow-other='no' and explicit target of '.'">
<x:context href="json-value_flag_constrained-closed_metaschema.xml"/>
<x:expect label="the resulting JSON Schema should have an enum to enforce it."
test="$x:result => j:json-to-xml()" select="'../json-value_flag_constrained-closed.json' => j:unparsed-text() => j:json-to-xml()"/>
test="j:json-to-xml($x:result)//j:map[@key='test-flag']"
select="j:json-to-xml(j:unparsed-text('../json-value_flag_constrained-closed.json'))//j:map[@key='test-flag']"/>
</x:scenario>
<x:scenario label="with a constraint, allowed-values, no explicit target, and permissive enforcement of allow-other='yes'">
<x:context href="json-value_flag_constrained-open_metaschema.xml"/>
<x:expect label="the resulting JSON Schema should not have an enum to enforce it."
test="$x:result => j:json-to-xml()" select="'../json-value_flag_constrained-open.json' => j:unparsed-text() => j:json-to-xml()"/>
test="j:json-to-xml($x:result)//j:map[@key='test-flag']"
select="j:json-to-xml(j:unparsed-text('../json-value_flag_constrained-open.json'))//j:map[@key='test-flag']"/>
</x:scenario>
</x:scenario>
<x:scenario label="if it has a defined field">
<!--
For allowed-value enumeration tests, the XSLT implementation's JSON schema gen prefixes the model short-name for its field definitions.
So, for the field 'test-field' in a Metaschema module with a short-name 'json-value_field_unconstrained' the final generated element
of the JSON schema struct is 'json-value_field_unconstrained-json-value_field_unconstrained:test-field.' This is why we do not match
on full field name in JSON schema struct by key from fn:json-to-xml() document-node() with the JSON data in XML form, and then use
fn: contains(@key, ':test-field') test to make tests less flaky if the module name in test data changes and just check the field name.
-->
<x:scenario label="with no constraint and no allowed-values">
<x:context href="json-value_field_unconstrained_metaschema.xml"/>
<x:expect label="the resulting JSON Schema should not have an enum to enforce it."
test="$x:result => j:json-to-xml()" select="'../json-value_field_unconstrained.json' => j:unparsed-text() => j:json-to-xml()"/>
test="j:json-to-xml($x:result)//j:map[contains(@key, ':test-field')]"
select="j:json-to-xml(j:unparsed-text('../json-value_field_unconstrained.json'))//j:map[contains(@key, ':test-field')]"/>
</x:scenario>
<x:scenario label="with a constraint, allowed-values, strict enforcement of allow-other='no' and explicit target of '.'">
<x:context href="json-value_field_constrained-closed_metaschema.xml"/>
<x:expect label="the resulting JSON Schema should have an enum to enforce it."
test="$x:result => j:json-to-xml()" select="'../json-value_field_constrained-closed.json' => j:unparsed-text() => j:json-to-xml()"/>
test="j:json-to-xml($x:result)//j:map[contains(@key, ':test-field')]"
select="j:json-to-xml(j:unparsed-text('../json-value_field_constrained-closed.json'))//j:map[contains(@key, ':test-field')]"/>
</x:scenario>
<x:scenario label="with a constraint, allowed-values, no explicit target, and permissive enforcement of allow-other='yes'">
<x:context href="json-value_field_constrained-open_metaschema.xml"/>
<x:expect label="the resulting JSON Schema should not have an enum to enforce it."
test="$x:result => j:json-to-xml()" select="'../json-value_field_constrained-open.json' => j:unparsed-text() => j:json-to-xml()"/>
test="j:json-to-xml($x:result)//j:map[contains(@key, ':test-field')]"
select="j:json-to-xml(j:unparsed-text('../json-value_field_constrained-open.json'))//j:map[contains(@key, ':test-field')]"/>
</x:scenario>
<x:scenario label="with a constraint, allowed-values, strict enforcement of allow-other='no' and explicit target other than '.'">
<x:context href="json-value_field_constrained-narrow_metaschema.xml"/>
<x:expect label="the resulting JSON Schema should not have an enum to enforce it."
test="$x:result => j:json-to-xml()" select="'../json-value_field_constrained-narrow.json' => j:unparsed-text() => j:json-to-xml()"/>
test="j:json-to-xml($x:result)//j:map[contains(@key, ':test-field')]"
select="j:json-to-xml(j:unparsed-text('../json-value_field_constrained-narrow.json'))//j:map[contains(@key, ':test-field')]"/>
</x:scenario>
<x:scenario label="with a constraint, allowed-values, permissive enforcement of allow-other='yes' and explicit target other than '.'">
<x:context href="json-value_field_constrained-sortof_metaschema.xml"/>
<x:expect label="the resulting JSON Schema should not have an enum to enforce it."
test="$x:result => j:json-to-xml()" select="'../json-value_field_constrained-sortof.json' => j:unparsed-text() => j:json-to-xml()"/>
test="j:json-to-xml($x:result)//j:map[contains(@key, ':test-field')]"
select="j:json-to-xml(j:unparsed-text('../json-value_field_constrained-sortof.json'))//j:map[contains(@key, ':test-field')]"/>
</x:scenario>
<x:scenario label="if has a $schema directive and does not import other Metaschema modules">
<x:context href="schema_directive_no-imports.xml"/>
<x:expect label="the resulting JSON Schema does not require but optionally allows the $schema declaration in document instances."
test="j:json-to-xml($x:result)//j:map[@key='properties']/j:map[@key='$schema']" select="j:json-to-xml(j:unparsed-text('../schema_directive_no-imports.json'))//j:map[@key='properties']/j:map[@key='$schema']"/>
</x:scenario>
<x:scenario label="if has a $schema directive and does import other Metaschema modules">
<x:context href="schema_directive_importing.xml"/>
<x:expect label="the resulting JSON Schema does not require but optionally allows the $schema declaration in document instances."
test="j:json-to-xml($x:result)//j:map[@key='properties']/j:map[@key='$schema']" select="j:json-to-xml(j:unparsed-text('../schema_directive_importing.json'))//j:map[@key='properties']/j:map[@key='$schema']"/>
</x:scenario>
</x:scenario>
</x:scenario>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/css" href="metaschema-author.css"?>
<METASCHEMA xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://csrc.nist.gov/ns/oscal/metaschema/1.0 ../../../schema/xml/metaschema.xsd"
xmlns="http://csrc.nist.gov/ns/oscal/metaschema/1.0" abstract="no">
<schema-name>JSON value testing mini metaschema</schema-name>
<schema-version>0.1</schema-version>
<short-name>schema-directive-imported1</short-name>
<namespace>http://csrc.nist.gov/ns/metaschema-tests/1.0</namespace>
<json-base-uri>http://csrc.nist.gov/ns/metaschema-tests</json-base-uri>
<import href="schema_directive_imported.xml"/>
<define-assembly name="root1">
<formal-name>Root</formal-name>
<description>Example root to test for schema directive in the imported module.</description>
<root-name>root1</root-name>
</define-assembly>
</METASCHEMA>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/css" href="metaschema-author.css"?>
<METASCHEMA xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://csrc.nist.gov/ns/oscal/metaschema/1.0 ../../../schema/xml/metaschema.xsd"
xmlns="http://csrc.nist.gov/ns/oscal/metaschema/1.0" abstract="no">
<schema-name>JSON value testing mini metaschema</schema-name>
<schema-version>0.1</schema-version>
<short-name>schema-directive-imported2</short-name>
<namespace>http://csrc.nist.gov/ns/metaschema-tests/1.0</namespace>
<json-base-uri>http://csrc.nist.gov/ns/metaschema-tests</json-base-uri>
<import href="schema_directive_imported.xml"/>
<define-assembly name="root2">
<formal-name>Root</formal-name>
<description>Example root to test for schema directive in the imported module.</description>
<root-name>root2</root-name>
</define-assembly>
</METASCHEMA>
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "http://csrc.nist.gov/ns/metaschema-tests/0.1/schema-directive-no-imports-schema.json",
"$comment": "JSON value testing mini metaschema: JSON Schema",
"type": "object",
"definitions": {
"json-schema-directive": {
"title": "Schema Directive",
"description": "A JSON Schema directive to bind a specific schema to its document instance.",
"$id": "#json-schema-directive",
"$ref": "#/definitions/URIReferenceDatatype"
},
"schema-directive-no-imports-schema-directive-imported1:root1": {
"title": "Root",
"description": "Example root to test for schema directive in the imported module.",
"$id": "#assembly_schema-directive-imported1_root1",
"type": "object",
"additionalProperties": false
},
"schema-directive-no-imports-schema-directive-imported2:root2": {
"title": "Root",
"description": "Example root to test for schema directive in the imported module.",
"$id": "#assembly_schema-directive-imported2_root2",
"type": "object",
"additionalProperties": false
},
"URIReferenceDatatype": {
"description": "A URI Reference, either a URI or a relative-reference, formatted according to section 4.1 of RFC3986.",
"type": "string",
"format": "uri-reference"
}
},
"oneOf": [
{
"properties": {
"$schema": {
"$ref": "#json-schema-directive"
},
"root1": {
"$ref": "#assembly_schema-directive-imported1_root1"
}
},
"required": [
"root1"
],
"additionalProperties": false
},
{
"properties": {
"$schema": {
"$ref": "#json-schema-directive"
},
"root2": {
"$ref": "#assembly_schema-directive-imported2_root2"
}
},
"required": [
"root2"
],
"additionalProperties": false
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/css" href="metaschema-author.css"?>
<METASCHEMA xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://csrc.nist.gov/ns/oscal/metaschema/1.0 ../../../schema/xml/metaschema.xsd"
xmlns="http://csrc.nist.gov/ns/oscal/metaschema/1.0" abstract="no">
<schema-name>JSON value testing mini metaschema</schema-name>
<schema-version>0.1</schema-version>
<short-name>schema-directive-no-imports</short-name>
<namespace>http://csrc.nist.gov/ns/metaschema-tests/1.0</namespace>
<json-base-uri>http://csrc.nist.gov/ns/metaschema-tests</json-base-uri>
<import href="schema_directive_imported1.xml"/>
<import href="schema_directive_imported2.xml"/>
</METASCHEMA>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "schema_directive_importing.json",
"root1": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"root2": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "http://csrc.nist.gov/ns/metaschema-tests/0.1/schema-directive-no-imports-schema.json",
"$comment": "JSON value testing mini metaschema: JSON Schema",
"type": "object",
"definitions": {
"json-schema-directive": {
"title": "Schema Directive",
"description": "A JSON Schema directive to bind a specific schema to its document instance.",
"$id": "#json-schema-directive",
"$ref": "#/definitions/URIReferenceDatatype"
},
"schema-directive-no-imports-schema-directive-no-imports:root": {
"title": "Root",
"description": "Example root to test for schema directive in simple module.",
"$id": "#assembly_schema-directive-no-imports_root",
"type": "object",
"additionalProperties": false
},
"URIReferenceDatatype": {
"description": "A URI Reference, either a URI or a relative-reference, formatted according to section 4.1 of RFC3986.",
"type": "string",
"format": "uri-reference"
}
},
"properties": {
"$schema": {
"$ref": "#json-schema-directive"
},
"root": {
"$ref": "#assembly_schema-directive-no-imports_root"
}
},
"required": [
"root"
],
"additionalProperties": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/css" href="metaschema-author.css"?>
<METASCHEMA xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://csrc.nist.gov/ns/oscal/metaschema/1.0 ../../../schema/xml/metaschema.xsd"
xmlns="http://csrc.nist.gov/ns/oscal/metaschema/1.0" abstract="no">
<schema-name>JSON value testing mini metaschema</schema-name>
<schema-version>0.1</schema-version>
<short-name>schema-directive-no-imports</short-name>
<namespace>http://csrc.nist.gov/ns/metaschema-tests/1.0</namespace>
<json-base-uri>http://csrc.nist.gov/ns/metaschema-tests</json-base-uri>
<define-assembly name="root">
<formal-name>Root</formal-name>
<description>Example root to test for schema directive in simple module.</description>
<root-name>root</root-name>
</define-assembly>
</METASCHEMA>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "schema_directive_no-imports.json",
"root": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"root": {}
}
14 changes: 11 additions & 3 deletions toolchains/xslt-M4/schema-gen/make-json-schema-metamap.xsl
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,16 @@

<string key="type">object</string>
<map key="definitions">
<map key="json-schema-directive">
<string key="title">Schema Directive</string>
<string key="description">A JSON Schema directive to bind a specific schema to its document instance.</string>
<string key="$id">#json-schema-directive</string>
<string key="$ref">#/definitions/URIReferenceDatatype</string>
</map>
<xsl:apply-templates select="define-assembly | define-field"/>

<xsl:variable name="all-used-types" select="//@as-type => distinct-values()"/>
<xsl:variable name="schema-directive-type" select="'uri-reference'"/>
<xsl:variable name="all-used-types" select="(//@as-type, $schema-directive-type) => distinct-values()"/>
<xsl:variable name="used-atomic-types" select="$datatype-map[@as-type = $all-used-types]"/>
<xsl:variable name="invoked-atomic-types" select="$used-atomic-types/key('datatypes-by-name',string(.),$datatypes)"/>
<xsl:variable name="referenced-types" select="$invoked-atomic-types//*:string[@key='$ref']/substring-after(.,'#/definitions/') ! key('datatypes-by-name',string(.),$datatypes)"/>
Expand All @@ -57,7 +64,6 @@
<xsl:template match="/METASCHEMA" mode="require-a-root">
<xsl:apply-templates select="define-assembly[exists(root-name)]" mode="root-requirement"/>
<boolean key="additionalProperties">false</boolean>
<number key="maxProperties">1</number>
</xsl:template>


Expand All @@ -67,14 +73,16 @@
<map>
<xsl:apply-templates select="." mode="root-requirement"/>
<boolean key="additionalProperties">false</boolean>
<number key="maxProperties">1</number>
</map>
</xsl:for-each>
</array>
</xsl:template>

<xsl:template match="define-assembly" mode="root-requirement">
<map key="properties">
<map key="$schema">
<string key="$ref">#json-schema-directive</string>
</map>
<map key="{root-name}">
<xsl:apply-templates select="." mode="make-ref"/>
</map>
Expand Down

0 comments on commit a36f579

Please sign in to comment.