"""
Generate JSON-LD contexts
"""
import os
import re
from typing import Union, TextIO, Set, Optional
import click
from jsonasobj2 import JsonObj, as_json
from linkml_runtime.linkml_model.meta import SchemaDefinition, ClassDefinition, SlotDefinition, Definition, Element
from linkml_runtime.linkml_model.types import SHEX
from linkml_runtime.utils.formatutils import camelcase, underscore, be
from rdflib import XSD, SKOS
from linkml.utils.generator import Generator, shared_arguments
URI_RANGES = (XSD.anyURI, SHEX.nonliteral, SHEX.bnode, SHEX.iri)
ENUM_CONTEXT = {
"@vocab": "@null",
"text": "skos:notation",
"description": "skos:prefLabel",
"meaning": "@id"
}
[docs]class ContextGenerator(Generator):
generatorname = os.path.basename(__file__)
generatorversion = "0.1.1"
valid_formats = ['context', 'json']
visit_all_class_slots = False
def __init__(self,
schema: Union[str, TextIO, SchemaDefinition],
**kwargs) -> None:
super().__init__(schema, **kwargs)
if self.namespaces is None:
raise TypeError("Schema text must be supplied to context generater. Preparsed schema will not work")
self.emit_prefixes: Set[str] = set()
self.default_ns = None
self.context_body = dict()
self.slot_class_maps = dict()
[docs] def visit_schema(self, base: Optional[str]=None, output: Optional[str]=None, **_):
# Add any explicitly declared prefixes
for prefix in self.schema.prefixes.values():
self.emit_prefixes.add(prefix.prefix_prefix)
# Add any prefixes explicitly declared
for pfx in self.schema.emit_prefixes:
self.add_prefix(pfx)
# Add the default prefix
if self.schema.default_prefix:
dflt = self.namespaces.prefix_for(self.schema.default_prefix)
if dflt:
self.default_ns = dflt
if self.default_ns:
default_uri = self.namespaces[self.default_ns]
self.emit_prefixes.add(self.default_ns)
else:
default_uri=self.schema.default_prefix
if self.schema.name:
self.namespaces[self.schema.name] = default_uri
self.emit_prefixes.add(self.schema.name)
self.context_body['@vocab'] = default_uri
# self.context_body['@base'] = self.base_dir
[docs] def end_schema(self,
base: Optional[str] = None,
output: Optional[str] = None,
prefixes: Optional[bool] = True,
flatprefixes: Optional[bool] = False,
model: Optional[bool] = True, **_) -> None:
context = JsonObj()
if self.emit_metadata:
comments = f'''Auto generated from {self.schema.source_file} by {self.generatorname} version: {self.generatorversion}'''
if self.schema.generation_date:
comments += f'''
Generation date: {self.schema.generation_date}
Schema: {self.schema.name}
metamodel version: {self.schema.metamodel_version}
model version: {self.schema.version if self.schema.version else None}
'''
comments += f'''
id: {self.schema.id}
description: {be(self.schema.description)}
license: {be(self.schema.license)}
'''
context["_comments"] = comments
context_content = {}
if base:
if '://' not in base:
self.context_body['@base'] = os.path.relpath(base, os.path.dirname(self.schema.source_file))
else:
self.context_body['@base'] = base
if prefixes:
for prefix in sorted(self.emit_prefixes):
url = str(self.namespaces[prefix])
# Derived from line # ~5223 in pyld/lib/jsonld.py
if bool(re.match(r'.*[:/\?#\[\]@]$', url)) or flatprefixes:
context_content[prefix] = url
else:
prefix_obj = JsonObj()
prefix_obj["@id"] = url
prefix_obj["@prefix"] = True
context_content[prefix] = prefix_obj
if model:
for k, v in self.context_body.items():
context_content[k] = v
for k, v in self.slot_class_maps.items():
context_content[k] = v
context['@context'] = context_content
if output:
with open(output, 'w', encoding='UTF-8') as outf:
outf.write(as_json(context))
else:
print(as_json(context))
[docs] def visit_class(self, cls: ClassDefinition) -> bool:
class_def = {}
cn = camelcase(cls.name)
self.add_mappings(cls)
cls_uri_prefix, cls_uri_suffix = self.namespaces.prefix_suffix(cls.class_uri)
if not self.default_ns or not cls_uri_prefix or cls_uri_prefix != self.default_ns:
class_def['@id'] = (cls_uri_prefix + ':' + cls_uri_suffix) if cls_uri_prefix else cls.class_uri
if cls_uri_prefix:
self.add_prefix(cls_uri_prefix)
if class_def:
self.slot_class_maps[cn] = class_def
# We don't bother to visit class slots - just all slots
return False
[docs] def visit_slot(self, aliased_slot_name: str, slot: SlotDefinition) -> None:
if slot.identifier:
slot_def = '@id'
else:
slot_def = {}
if not slot.usage_slot_name:
if slot.range in self.schema.classes:
slot_def['@type'] = '@id'
elif slot.range in self.schema.enums:
slot_def['@context'] = ENUM_CONTEXT
# Add the necessary prefixes to the namespace
skos = self.namespaces.prefix_for(SKOS)
if not skos:
self.namespaces['skos'] = SKOS
skos = 'skos'
self.emit_prefixes.add(skos)
else:
range_type = self.schema.types[slot.range]
if self.namespaces.uri_for(range_type.uri) == XSD.string:
pass
elif self.namespaces.uri_for(range_type.uri) in URI_RANGES:
slot_def['@type'] = '@id'
else:
slot_def['@type'] = range_type.uri
slot_prefix = self.namespaces.prefix_for(slot.slot_uri)
if not self.default_ns or not slot_prefix or slot_prefix != self.default_ns:
slot_def['@id'] = slot.slot_uri
if slot_prefix:
self.add_prefix(slot_prefix)
self.add_mappings(slot)
if slot_def:
self.context_body[underscore(aliased_slot_name)] = slot_def
@shared_arguments(ContextGenerator)
@click.command()
@click.option("--base", help="Base URI for model")
@click.option("--prefixes/--no-prefixes", default=True, show_default=True, help="Emit context for prefixes (default=--prefixes)")
@click.option("--model/--no-model", default=True, show_default=True, help="Emit context for model elements (default=--model)")
@click.option("--flatprefixes/--no-flatprefixes", default=False, show_default=True, help="Emit non-JSON-LD compliant prefixes as an object (deprecated: use gen-prefix-map instead).")
def cli(yamlfile, **args):
""" Generate jsonld @context definition from LinkML model """
print(ContextGenerator(yamlfile, **args).serialize(**args))
if __name__ == '__main__':
cli()