diff --git a/ogc/bblocks/examples-schema.yaml b/ogc/bblocks/examples-schema.yaml index c54350c..c148a8f 100644 --- a/ogc/bblocks/examples-schema.yaml +++ b/ogc/bblocks/examples-schema.yaml @@ -1,135 +1,177 @@ "$schema": https://json-schema.org/draft/2020-12/schema title: OGC Building Blocks examples schema -type: array -items: - type: object - anyOf: - - required: - - content - - required: - - snippets - properties: - snippets: - minItems: 1 - properties: - title: - description: A title for this example - type: string - content: - description: Markdown contents to describe the example. - type: string - base-uri: - description: Base URI that will be used for semantic uplift (JSON -> JSON-LD -> Turtle). - type: string - base-output-filename: - description: | - Base filename that will be used for writing this example to its own file. Extension, if any, will be discarded. - type: string - snippets: - description: | - Collection of snippets to illustrate this example. Preferably, only one snippet per language - should be added here, as additional snippets in the same language can be created as different - examples. - type: array - items: - type: object - required: - - language +$defs: + + examples: + type: array + items: + $ref: '#/$defs/example' + + example: + type: object + anyOf: + - required: + - content + - required: + - snippets properties: - language: - description: | - The language for this snippet. Can be a file format (such as 'jsonld' or 'turtle'), - a MIME type (e.g., 'text/html'), a programming language ('python'). 'plaintext' by default. - type: string - code: - description: Code block for the snippet. Either this property or 'ref' have to be provided. - type: string - ref: - description: | - A reference to a filename (relative to the examples.yaml file) with the contents of - this snippet, as an alternative to inlining them in the 'code' property. - type: string - format: uri-reference - base-uri: - description: | - Base URI that will be used when semantically uplifting this snippet. Overrides - that of the example, if any. - type: string - schema-ref: - description: | - A reference to a JSON schema or subschema that will be used to validate this snippet. - A full URL or a filename (relative to the building block's 'schema.yaml'), with or - without a fragment, can be provided. If only a fragment is provided, it will be - looked up inside the default 'schema.yaml'. If none provided, the default schema for - the building block will be employed. - type: string - format: uri-reference - doc-uplift-formats: - description: | - Uplifted snippet format, or array thereof, that will be added to the output documentation. - If omitted, both 'jsonld' and 'ttl' will be used; if empty array or null, no uplifted snippets - will be included in the documentation. - oneOf: - - type: string - enum: [jsonld, ttl] - - type: array - items: - type: string - enum: [jsonld, ttl] - - type: 'null' - shacl-closure: - description: | - List of Turtle documents (file names or URLs) that will be used as the SHACL closure graph. This list - will be merged with the one defined inside `bblock.json`, if any. - type: array + snippets: + minItems: 1 + properties: + title: + description: A title for this example + type: string + content: + description: Markdown contents to describe the example. + type: string + base-uri: + description: Base URI that will be used for semantic uplift (JSON -> JSON-LD -> Turtle). + type: string + base-output-filename: + description: | + Base filename that will be used for writing this example to its own file. Extension, if any, will be discarded. + type: string + prefixes: + description: | + Prefixes for this example. Will be merged with top-level `prefixes`, if any, with the example + ones taking precedence. + $ref: '#/$defs/prefixes' + snippets: + description: | + Collection of snippets to illustrate this example. Preferably, only one snippet per language + should be added here, as additional snippets in the same language can be created as different + examples. + type: array + items: + $ref: '#/$defs/snippet' + + transforms: + description: | + List of transforms for this example + type: array + items: + $ref: '#/$defs/transform' + + snippet: + type: object + required: + - language + properties: + language: + description: | + The language for this snippet. Can be a file format (such as 'jsonld' or 'turtle'), + a MIME type (e.g., 'text/html'), a programming language ('python'). 'plaintext' by default. + type: string + code: + description: Code block for the snippet. Either this property or 'ref' have to be provided. + type: string + ref: + description: | + A reference to a filename (relative to the examples.yaml file) with the contents of + this snippet, as an alternative to inlining them in the 'code' property. + type: string + format: uri-reference + base-uri: + description: | + Base URI that will be used when semantically uplifting this snippet. Overrides + that of the example, if any. + type: string + schema-ref: + description: | + A reference to a JSON schema or subschema that will be used to validate this snippet. + A full URL or a filename (relative to the building block's 'schema.yaml'), with or + without a fragment, can be provided. If only a fragment is provided, it will be + looked up inside the default 'schema.yaml'. If none provided, the default schema for + the building block will be employed. + type: string + format: uri-reference + doc-uplift-formats: + description: | + Uplifted snippet format, or array thereof, that will be added to the output documentation. + If omitted, both 'jsonld' and 'ttl' will be used; if empty array or null, no uplifted snippets + will be included in the documentation. + oneOf: + - type: string + enum: [ jsonld, ttl ] + - type: array items: type: string - expand-level: - description: Default expand level for Treedoc Viewer, where applicable (JSON, YAML) - type: integer - minimum: 1 - oneOf: - - required: - - code - - required: - - ref - transforms: - description: | - List of transforms for this example - type: array - items: - type: object - required: - - input-language - - output-language - - type - oneOf: - - required: - - code - - required: - - ref - properties: - input-language: - description: | - Input language of the code snippet from this example that will be transformed. It can correspond to a - manually-provided snippet, or to an uplifted one. - type: string - output-language: - description: | - Output language of the transformed snippet. - type: string - type: - description: | - The type of this transform. "jq" or "shacl" are examples of automatically processed ones. - type: string - description: - description: Textual description of this transformation. Markdown is accepted. - type: string - code: - description: Code contents of this transformation (e.g., jq script or SHACL rules file). - type: string - ref: - description: | - Location of a file with the code contents of this transformation (instead of - providing them inline through the "code" property). - type: string + enum: [ jsonld, ttl ] + - type: 'null' + shacl-closure: + description: | + List of Turtle documents (file names or URLs) that will be used as the SHACL closure graph. This list + will be merged with the one defined inside `bblock.json`, if any. + type: array + items: + type: string + expand-level: + description: Default expand level for Treedoc Viewer, where applicable (JSON, YAML) + type: integer + minimum: 1 + oneOf: + - required: + - code + - required: + - ref + + transform: + type: object + required: + - input-language + - output-language + - type + oneOf: + - required: + - code + - required: + - ref + properties: + input-language: + description: | + Input language of the code snippet from this example that will be transformed. It can correspond to a + manually-provided snippet, or to an uplifted one. + type: string + output-language: + description: | + Output language of the transformed snippet. + type: string + type: + description: | + The type of this transform. "jq" or "shacl" are examples of automatically processed ones. + type: string + description: + description: Textual description of this transformation. Markdown is accepted. + type: string + code: + description: Code contents of this transformation (e.g., jq script or SHACL rules file). + type: string + ref: + description: | + Location of a file with the code contents of this transformation (instead of + providing them inline through the "code" property). + type: string + + prefixes: + type: object + description: Map of prefix -> URI that will be used for Turtle resources + additionalProperties: + type: string + examples: + - dct: http://purl.org/dc/terms/ + dcat: http://www.w3.org/ns/dcat# + - skos: http://www.w3.org/2004/02/skos/core# + rdfs: http://www.w3.org/2000/01/rdf-schema# + schema: http://schema.org/ + +oneOf: + - $ref: '#/$defs/examples' + - type: object + properties: + prefixes: + description: Default prefixes for all examples + $ref: '#/$defs/prefixes' + examples: + $ref: '#/$defs/examples' + required: + - examples \ No newline at end of file diff --git a/ogc/bblocks/models.py b/ogc/bblocks/models.py index 8a9a7c4..04c299c 100644 --- a/ogc/bblocks/models.py +++ b/ogc/bblocks/models.py @@ -82,7 +82,7 @@ def __init__(self, identifier: str, metadata_file: Path, self.assets_path = ap if ap.is_dir() else None self.examples_file = fp / 'examples.yaml' - self.examples = self._load_examples() + self._load_examples() self.tests_dir = fp / 'tests' @@ -132,6 +132,7 @@ def get(self, item, default=None): def _load_examples(self): examples = None + prefixes = {} if self.examples_file.is_file(): examples = load_yaml(self.examples_file) if not examples: @@ -141,6 +142,10 @@ def _load_examples(self): except Exception as e: raise BuildingBlockError('Error validating building block examples (examples.yaml)') from e + if isinstance(examples, dict): + prefixes = examples.get('prefixes', {}) + examples = examples['examples'] + for example in examples: for snippet in example.get('snippets', ()): if 'ref' in snippet: @@ -152,7 +157,11 @@ def _load_examples(self): # Load transform code from "ref" ref = transform['ref'] if is_url(transform['ref']) else self.files_path / transform['ref'] transform['code'] = load_file(ref) - return examples + if prefixes: + example['prefixes'] = {**prefixes, **example.get('prefixes', {})} + + self.example_prefixes = prefixes + self.examples = examples @property def schema_contents(self): diff --git a/ogc/bblocks/templates/json-full/index.json b/ogc/bblocks/templates/json-full/index.json index 5d0b4b0..da28f99 100644 --- a/ogc/bblocks/templates/json-full/index.json +++ b/ogc/bblocks/templates/json-full/index.json @@ -9,6 +9,8 @@ if bblock.description: output['description'] = bblock.description if bblock.examples: output['examples'] = bblock.examples +if bblock.example_prefixes: + output['examplePrefixes'] = bblock.example_prefixes if bblock.annotated_schema: output['annotatedSchema'] = bblock.annotated_schema_contents if git_repo: diff --git a/ogc/bblocks/validate.py b/ogc/bblocks/validate.py index 87ca32c..0267458 100644 --- a/ogc/bblocks/validate.py +++ b/ogc/bblocks/validate.py @@ -150,7 +150,8 @@ def _validate_resource(bblock: BuildingBlock, schema_ref: str | None = None, require_fail: bool | None = None, resource_url: str | None = None, - example_index: tuple[int, int] | None = None) -> ValidationReportItem | None: + example_index: tuple[int, int] | None = None, + prefixes: dict[str, str] | None = None) -> ValidationReportItem | None: if require_fail is None: require_fail = filename.stem.endswith('-fail') and not example_index @@ -184,7 +185,8 @@ def _validate_resource(bblock: BuildingBlock, contents=resource_contents, base_uri=base_uri, additional_shacl_closures=additional_shacl_closures, - schema_ref=schema_ref) + schema_ref=schema_ref, + prefixes=prefixes) any_validator_run = any_validator_run or (result is not False) except Exception as unknown_exc: @@ -192,7 +194,7 @@ def _validate_resource(bblock: BuildingBlock, section=ValidationReportSection.UNKNOWN, message=','.join(traceback.format_exception(unknown_exc)), is_error=True, - is_global=True, + is_global=False, payload={ 'exception': unknown_exc.__class__.__qualname__, } @@ -324,6 +326,7 @@ def validate_test_resources(bblock: BuildingBlock, schema_ref=snippet.get('schema-ref'), resource_url=snippet.get('ref'), require_fail=False, + prefixes=example.get('prefixes'), ) if example_result: all_results.append(example_result) diff --git a/ogc/bblocks/validation/__init__.py b/ogc/bblocks/validation/__init__.py index a87d5ac..466b0ec 100644 --- a/ogc/bblocks/validation/__init__.py +++ b/ogc/bblocks/validation/__init__.py @@ -23,6 +23,7 @@ def validate(self, filename: Path, output_filename: Path, base_uri: str | None = None, resource_url: str | None = None, require_fail: bool | None = None, + prefixes: dict[str, str] | None = None, **kwargs) -> bool | None: raise NotImplementedError diff --git a/ogc/bblocks/validation/rdf.py b/ogc/bblocks/validation/rdf.py index 46276fb..0ff84bc 100644 --- a/ogc/bblocks/validation/rdf.py +++ b/ogc/bblocks/validation/rdf.py @@ -116,13 +116,13 @@ def __init__(self, bblock: BuildingBlock, register: BuildingBlockRegister): self.shacl_errors.append(f"Error processing {shacl_file}: {str(e)}") inherited_shacl_rules[shacl_bblock] = bblock_shacl_files - for shacle_closure in bblock.shaclClosures or (): + for shacl_closure in bblock.shaclClosures or (): try: - self.closure_graph.parse(bblock.resolve_file(shacle_closure), format='turtle') + self.closure_graph.parse(bblock.resolve_file(shacl_closure), format='turtle') except HTTPError as e: self.shacl_errors.append(f"Error retrieving {e.url}: {e}") except Exception as e: - self.shacl_errors.append(f"Error processing {shacle_closure}: {str(e)}") + self.shacl_errors.append(f"Error processing {shacl_closure}: {str(e)}") bblock.metadata['shaclRules'] = inherited_shacl_rules @@ -130,7 +130,8 @@ def __init__(self, bblock: BuildingBlock, register: BuildingBlockRegister): def _load_graph(self, filename: Path, output_filename: Path, report: ValidationReportItem, contents: str | None = None, - base_uri: str | None = None) -> Graph | None | bool: + base_uri: str | None = None, + prefixes: dict[str, str] | None = None) -> Graph | None | bool: graph = False if filename.suffix == '.json': if self.jsonld_error: @@ -242,6 +243,13 @@ def _load_graph(self, filename: Path, output_filename: Path, report: ValidationR rdf_format = 'ttl' if output_filename.suffix == '.ttl' else 'json-ld' try: if contents: + # Prepend prefixes + if prefixes: + contents = '\n'.join(f"@prefix {k}: <{v}> ." for k, v in prefixes.items()) + '\n' + contents + report.add_entry(ValidationReportEntry( + section=ValidationReportSection.TURTLE, + message=f"Prefixes are defined for {', '.join(prefixes.keys())}" + )) graph = Graph().parse(data=contents, format=rdf_format) report.add_entry(ValidationReportEntry( section=ValidationReportSection.FILES, @@ -270,8 +278,9 @@ def validate(self, filename: Path, output_filename: Path, report: ValidationRepo contents: str | None = None, base_uri: str | None = None, additional_shacl_closures: list[str | Path] | None = None, + prefixes: dict[str, str] | None = None, **kwargs) -> bool | None: - graph = self._load_graph(filename, output_filename, report, contents, base_uri) + graph = self._load_graph(filename, output_filename, report, contents, base_uri, prefixes) if graph is False: return False