diff --git a/definition_schema.json b/definition_schema.json new file mode 100644 index 00000000..c4c5ad4d --- /dev/null +++ b/definition_schema.json @@ -0,0 +1,217 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/schema", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true, + "https://json-schema.org/draft/2020-12/vocab/unevaluated": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true, + "https://json-schema.org/draft/2020-12/vocab/meta-data": true, + "https://json-schema.org/draft/2020-12/vocab/format-annotation": true, + "https://json-schema.org/draft/2020-12/vocab/content": true + }, + "$dynamicAnchor": "meta", + "title": "Core and Validation specifications meta-schema", + "allOf": [ + { + "$ref": "meta/core" + }, + { + "$ref": "meta/applicator" + }, + { + "$ref": "meta/unevaluated" + }, + { + "$ref": "meta/validation" + }, + { + "$ref": "meta/meta-data" + }, + { + "$ref": "meta/format-annotation" + }, + { + "$ref": "meta/content" + } + ], + "unevaluatedProperties": false, + "type": [ + "object", + "boolean" + ], + "$comment": "This meta schema for the Eiffel Definitions files is a copy of a meta schema with some additions.", + "properties": { + "_description": { + "type": "string" + }, + "_abbrev": { + "type": "string" + }, + "_name": { + "type": "string" + }, + "_version": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + }, + "_format": { + "type": "string" + }, + "_examples": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + } + }, + "required": [ + "title", + "url" + ], + "additionalProperties": false + } + }, + "_history": { + "type": "array", + "items": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "introduced_in": { + "type": "string" + }, + "changes": { + "type": "string" + } + }, + "required": [ + "version", + "changes" + ], + "additionalProperties": false + } + }, + "_links": { + "type": "object", + "patternProperties": { + "^[A-Z_]+$": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "multiple": { + "type": "boolean" + }, + "experimental": { + "type": "boolean" + }, + "targets": { + "type": "object", + "properties": { + "any_type": { + "type": "boolean" + }, + "types": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "any_type", + "types" + ], + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "any_type": { + "const": true + } + } + }, + "then": { + "properties": { + "types": { + "maxItems": 0 + } + } + }, + "else": { + "properties": { + "types": { + "minItems": 1 + } + } + } + } + ] + } + }, + "required": [ + "description", + "required", + "multiple", + "targets" + ], + "additionalProperties": false + } + } + }, + "properties": { + "type": "object" + }, + "definitions": { + "$comment": "\"definitions\" has been replaced by \"$defs\".", + "type": "object", + "additionalProperties": { + "$dynamicRef": "#meta" + }, + "deprecated": true, + "default": {} + }, + "dependencies": { + "$comment": "\"dependencies\" has been split and replaced by \"dependentSchemas\" and \"dependentRequired\" in order to serve their differing semantics.", + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$dynamicRef": "#meta" + }, + { + "$ref": "meta/validation#/$defs/stringArray" + } + ] + }, + "deprecated": true, + "default": {} + }, + "$recursiveAnchor": { + "$comment": "\"$recursiveAnchor\" has been replaced by \"$dynamicAnchor\".", + "$ref": "meta/core#/$defs/anchorString", + "deprecated": true + }, + "$recursiveRef": { + "$comment": "\"$recursiveRef\" has been replaced by \"$dynamicRef\".", + "$ref": "meta/core#/$defs/uriReferenceString", + "deprecated": true + } + } +} diff --git a/eiffel-syntax-and-usage/event-schemas.md b/eiffel-syntax-and-usage/event-schemas.md index 1c64dd8f..61474727 100644 --- a/eiffel-syntax-and-usage/event-schemas.md +++ b/eiffel-syntax-and-usage/event-schemas.md @@ -23,26 +23,26 @@ The documentation of each event is available in a Markdown file that covers the Both schemas and documentation files are generated from _schema definition files_; YAML files with a custom schema based on the JSON schema specification. Like the schema files themselves there's one file per event and version, e.g. [definitions/EiffelCompositionDefinedEvent/3.2.0.yml](../definitions/EiffelCompositionDefinedEvent/3.2.0.yml). The main difference between these files and normal JSON schema files, apart from the YAML representation, is an additional set of keys with an underscore prefix that contains additional metadata that can't be part of the JSON schema itself. Those keys are used when generating the documentation and are simply ignored when the schema definition files are turned into schema files. The following table describes these additional top-level keys. -| Key | Description | -| --------------- | ----------------------- | -| `_name` | The name of the type, e.g. "EiffelCompositionDefinedEvent". This string must match the name of the parent directory. | -| `_version` | The version of the event or other type, e.g. "3.1.0". This string must match the name of the definition file, except for the filename suffix. | -| `_abbrev` | The abbreviation of the event name, e.g. "CD" for EiffelCompositionDefinedEvent. | -| `_description` | An overall description of the event. | -| `_links` | An object describing the valid link types for the event. | -| `_links..description` | A description of the link type. | -| `_links..required` | A boolean value indicating whether a link of this type is required. Optional, default false. | -| `_links..multiple` | A boolean value indicating whether multiple links of this type is allowed. Optional, default false. | -| `_links..experimental` | A boolean value indicating whether the link type is experimental, i.e. the only valid target is an experimental event type. Optional, default false. | -| `_links..targets.any_type` | A boolean value indicating whether the link can point to an event of any type. | -| `_links..targets.types` | A string array of event names that the link type may point to. Must be non-empty if `any_type` is false, and must be empty if `any_type` is true. | +| Key | Description | +| --------------- |--------------------------------------------------------------------------------------------------------------------------------------------------------| +| `_name` | The name of the type, e.g. "EiffelCompositionDefinedEvent". This string must match the name of the parent directory. | +| `_version` | The version of the event or other type, e.g. "3.1.0". This string must match the name of the definition file, except for the filename suffix. | +| `_abbrev` | The abbreviation of the event name, e.g. "CD" for EiffelCompositionDefinedEvent. | +| `_description` | An overall description of the event. | +| `_links` | An object describing the valid link types for the event. | +| `_links..description` | A description of the link type. | +| `_links..required` | A boolean value indicating whether a link of this type is required. | +| `_links..multiple` | A boolean value indicating whether multiple links of this type is allowed. | +| `_links..experimental` | A boolean value indicating whether the link type is experimental, i.e. the only valid target is an experimental event type. Optional, default false. | +| `_links..targets.any_type` | A boolean value indicating whether the link can point to an event of any type. | +| `_links..targets.types` | A string array of event names that the link type may point to. Must be non-empty if `any_type` is false, and must be empty if `any_type` is true. | | `_history` | An array of objects describing the event type's version history, up to and including the current version. The items should be sorted in reverse order. | -| `_history.version` | The event version described in this item. | -| `_history.introduced_in` | The Git tag of the first edition where this version was available, or null if the version hasn't been released in a protocol edition. | -| `_history.changes` | A short description of the changes in this item's event version. | -| `_examples` | An array of objects describing examples of this event. | -| `_examples.title` | The name of the example. | -| `_examples.url` | The URL of the example. | +| `_history.version` | The event version described in this item. | +| `_history.introduced_in` | The Git tag of the first edition where this version was available, or null if the version hasn't been released in a protocol edition. | +| `_history.changes` | A short description of the changes in this item's event version. | +| `_examples` | An array of objects describing examples of this event. | +| `_examples.title` | The name of the example. | +| `_examples.url` | The URL of the example. | In addition, the object that describes each property in the schema supports a few additional keys that are specific to the schema definition format and will be omitted when the schema file is produced: diff --git a/test_definitions.py b/test_definitions.py index 737b4fc9..b9be4817 100644 --- a/test_definitions.py +++ b/test_definitions.py @@ -12,9 +12,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +import json from pathlib import Path +import jsonschema import pytest import definition_loader @@ -50,6 +51,12 @@ def __init__(self, path: Path): OTHER_DEFINITION_IDS = [d.id for d in OTHER_DEFINITIONS] +@pytest.fixture(scope="session") +def yaml_validator(): + definition_schema = json.load(open("definition_schema.json")) + return jsonschema.Draft202012Validator(definition_schema) + + @pytest.fixture(scope="session") def manifest(): return generate_manifest.Manifest("event_manifest.yml") @@ -114,6 +121,24 @@ def test_filename_matches_type_version_fields(definition_file): assert version == definition_file.definition["_version"] +@pytest.mark.parametrize( + "definition_file", + EVENT_DEFINITIONS_W_LINKS_REQUIRED + OTHER_DEFINITIONS, + ids=EVENT_DEFINITION_W_LINKS_REQUIRED_IDS + OTHER_DEFINITION_IDS, +) +def test_schema_validation(definition_file, yaml_validator): + # If you try to just add on to the meta schema it doesn't work see this for more info + # https://stackoverflow.com/questions/54719010/how-do-you-extend-json-schema-meta-schema-to-support-new-properties + # + # You should be able to create a vocabulary, but I have not managed. See the following for more + # https://stackoverflow.com/questions/64138556/how-do-i-define-my-own-json-schema-keyword-and-vocabulary + # https://json-schema.org/draft/2019-09/json-schema-core#rfc.appendix.D + + # Note that additionalProperties and properties have been included again. Assuming that in the meta schema these + # properties can only reside in certain places not valid for our definition files + yaml_validator.validate(definition_file.definition) + + @pytest.mark.parametrize( "definition_file", EVENT_DEFINITIONS_W_LINKS_REQUIRED,