Source code for linkml.generators.typescriptgen

import logging
import os
from dataclasses import dataclass
from typing import List, Optional

import click
from jinja2 import Template
from linkml_runtime.linkml_model.meta import (
    ClassDefinition,
    ClassDefinitionName,
    Element,
    SlotDefinition,
    SlotDefinitionName,
)
from linkml_runtime.utils.formatutils import camelcase, underscore
from linkml_runtime.utils.schemaview import SchemaView

from linkml._version import __version__
from linkml.generators.oocodegen import OOCodeGenerator
from linkml.utils.generator import shared_arguments

type_map = {
    "str": "string",
    "int": "number",
    "Bool": "boolean",
    "float": "number",
    "XSDDate": "date",
}

type_init_map = {
    "str": "''",
    "int": "0",
    "Bool": "false",
    "float": "0.0",
    "XSDDate": "new Date()",
}

default_template = """
{%- for c in view.all_classes().values() -%}
{%- set cref = gen.classref(c) -%}
{% if cref -%}
export type {{cref}} = string;
{% endif -%}
{%- endfor -%}

{% for e in enums.values() %}
{%- if e.description -%}
/**
* {{e.description}}
*/
{%- endif %}
export enum {{e.name}} {
    {% if not e.values -%}
    dummy = "dummy"
    {%- endif %}
    {%- for _, pv in e['values'].items() %}
    {% if pv.description -%}
    /** {{pv.description}} */
    {% endif -%}
    {{pv.label}} = "{{pv.value}}",
    {%- endfor %}
};
{% endfor %}

{% for c in view.all_classes().values() -%}
{%- if c.description -%}
/**
 * {{c.description}}
 */
{%- endif -%}
{% set parents = gen.parents(c) %}
export interface {{gen.name(c)}} {%- if parents %} extends {{parents|join(', ')}} {%- endif %} {
    {%- for sn in view.class_slots(c.name, direct=not(gen.include_induced_slots)) %}
    {% set s = view.induced_slot(sn, c.name) %}
    {%- if s.description -%}
    /** {{s.description}} */
    {% endif -%}
    {{gen.name(s)}}{%- if not s.required -%}?{%- endif -%}: {{gen.range(s)}},
    {%- endfor %}
}

{% if gen.gen_type_utils %}
export function is{{gen.name(c)}}(o: object): o is {{gen.name(c)}} {
    {%- set rcs = gen.required_slots(c) %}
    {%- set comp = "&&" if rcs else "||" %}
    {%- set cs = rcs if rcs else view.class_slots(c.name, direct=False) %}
    return (
        {%- for sn in cs %}
        '{{sn}}' in o {%- if not loop.last %} {{comp}}{% endif -%}
        {%- endfor %}
    )
}

export function to{{gen.name(c)}}(o: {{gen.name(c)}}): {{gen.name(c)}} {
    return {
        {%- for sn in view.class_slots(c.name, direct=False) %}
        {%- set s = view.induced_slot(sn, c.name) %}
        {{sn}}: o.{{sn}} ?? {{gen.init_range(s)}}{%- if not loop.last %},{%- endif -%}
        {%- endfor %}
    }
}
{% endif %}
{% endfor %}
"""


[docs]@dataclass class TypescriptGenerator(OOCodeGenerator): """ Generates typescript from a schema """ # ClassVars generatorname = os.path.basename(__file__) generatorversion = "0.0.1" valid_formats = ["text"] uses_schemaloader = False # ObjectVars gen_type_utils: bool = False include_induced_slots: bool = False
[docs] def serialize(self) -> str: """Serialize a schema to typescript string""" sv: SchemaView = self.schemaview enums = self.generate_enums(sv.all_enums()) template_obj = Template(default_template) out_str = template_obj.render( gen=self, schema=self.schemaview.schema, view=self.schemaview, enums=enums, ) return out_str
@staticmethod def name(element: Element) -> str: """ Returns the name of the element in its canonical form :param element: :return: """ alias = element.name if isinstance(element, SlotDefinition) and element.alias: alias = element.alias if type(element).class_name == "slot_definition": return underscore(alias) else: return camelcase(alias) def classref(self, cls: ClassDefinition) -> Optional[str]: """ Returns the class name for the class that holds a reference (foreign key) to members of this class E.g. if a class Person has an identifier field called unique_id, then this will return PersonUniqueId :param cls: :return: ref name, None if no identifier """ id_slot = self.get_identifier_or_key_slot(cls.name) if id_slot: return f"{self.name(cls)}{camelcase(id_slot.name)}" else: return None def get_identifier_or_key_slot(self, cn: ClassDefinitionName) -> Optional[SlotDefinition]: sv = self.schemaview id_slot = sv.get_identifier_slot(cn) if id_slot: return id_slot else: for s in sv.class_induced_slots(cn): if s.key: return s return None def range(self, slot: SlotDefinition) -> str: sv = self.schemaview r = slot.range if r in sv.all_classes(): rc = sv.get_class(r) rc_ref = self.classref(rc) rc_name = self.name(rc) id_slot = self.get_identifier_or_key_slot(r) if slot.multivalued: if not id_slot or slot.inlined: if slot.inlined_as_list or not id_slot: return f"{rc_name}[]" else: return f"{{[index: {rc_ref}]: {rc_name} }}" else: return f"{rc_ref}[]" else: if not id_slot or slot.inlined: return rc_name else: return f"{rc_ref}" else: if r in sv.all_types(): t = sv.get_type(r) tsrange = "string" if t.base and t.base in type_map: tsrange = type_map[t.base] elif t.typeof and t.typeof in type_map: tsrange = type_map[t.typeof] else: logging.warning(f"Unknown type.base: {t.name}") if slot.multivalued: tsrange = f"{tsrange}[]" return tsrange return "string" def init_range(self, slot: SlotDefinition) -> str: sv = self.schemaview r = slot.range if r in sv.all_classes(): id_slot = self.get_identifier_or_key_slot(r) if slot.multivalued: if not id_slot or slot.inlined: if slot.inlined_as_list or not id_slot: return "[]" else: return "{}" else: return "[]" else: if not id_slot or slot.inlined: return "{}" else: return "null" else: if r in sv.all_types(): t = sv.get_type(r) if slot.multivalued: return "[]" elif t.base and t.base in type_map: return type_init_map[t.base] elif t.typeof and t.typeof in type_map: return type_init_map[t.typeof] else: logging.warning(f"Unknown type.base: {t.name}") return "null" return "null" @staticmethod def parents(cls: ClassDefinition) -> List[ClassDefinitionName]: if cls.is_a: parents = [cls.is_a] else: parents = [] return [ClassDefinitionName(camelcase(p)) for p in parents + cls.mixins] def default_value_for_type(self, typ: str) -> str: pass def required_slots(self, cls: ClassDefinition) -> List[SlotDefinitionName]: return [s for s in self.schemaview.class_slots(cls.name) if self.schemaview.induced_slot(s, cls.name).required]
@shared_arguments(TypescriptGenerator) @click.version_option(__version__, "-V", "--version") @click.option("--gen-type-utils/", "-u", help="Generate Type checking utils", is_flag=True) @click.option("--include-induced-slots/", help="Generate slots induced through inheritance", is_flag=True) @click.command() def cli(yamlfile, gen_type_utils=False, include_induced_slots=False, **args): """Generate typescript interfaces and types See https://github.com/linkml/linkml-runtime.js """ gen = TypescriptGenerator( yamlfile, gen_type_utils=gen_type_utils, include_induced_slots=include_induced_slots, **args ) print(gen.serialize()) if __name__ == "__main__": cli()