From d7f82d9d46a18fc11a27e98fea6e5fcf955f6450 Mon Sep 17 00:00:00 2001 From: Matthew Woehlke Date: Tue, 30 Apr 2024 17:12:10 -0400 Subject: [PATCH 1/3] Fix flake8 warnings in cpy.py Fix some style issues in our Sphinx extension, as reported by flake8. --- _extensions/cps.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/_extensions/cps.py b/_extensions/cps.py index 312664e..75828f4 100644 --- a/_extensions/cps.py +++ b/_extensions/cps.py @@ -1,17 +1,16 @@ -from sphinx import domains -from sphinx import addnodes +import re from docutils import nodes from docutils.parsers.rst import directives, roles from docutils.transforms import Transform -import re +from sphinx import addnodes, domains -#============================================================================== +# ============================================================================= class InternalizeLinks(Transform): default_priority = 200 - #-------------------------------------------------------------------------- + # ------------------------------------------------------------------------- def is_internal_link(self, refuri): if not refuri: return True @@ -21,7 +20,7 @@ def is_internal_link(self, refuri): return True - #-------------------------------------------------------------------------- + # ------------------------------------------------------------------------- def apply(self, **kwargs): for ref in self.document.findall(nodes.reference): # Skip inter-document links @@ -56,11 +55,11 @@ def apply(self, **kwargs): # Replace the old node ref.replace_self(xref) -#============================================================================== +# ============================================================================= class CpsDomain(domains.Domain): name = 'cps' - #-------------------------------------------------------------------------- + # ------------------------------------------------------------------------- def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -81,22 +80,22 @@ def __init__(self, *args, **kwargs): self.add_code_role('var') self.add_code_role('env') - #-------------------------------------------------------------------------- + # ------------------------------------------------------------------------- def add_role(self, name, styles=None, parent=roles.generic_custom_role): options = {} if styles is None: - styles=name + styles = name else: - styles=' '.join([name] + styles) + styles = ' '.join([name] + styles) options['class'] = directives.class_option(styles) self.roles[name] = roles.CustomRole(name, parent, options) - #-------------------------------------------------------------------------- + # ------------------------------------------------------------------------- def add_code_role(self, name, styles=None, parent=roles.code_role): self.add_role(name, styles, parent) -#============================================================================== +# ============================================================================= def setup(app): app.add_domain(CpsDomain) From c9269aba4ffb573e2f9322eb868e42d71626df67 Mon Sep 17 00:00:00 2001 From: Matthew Woehlke Date: Thu, 2 May 2024 16:24:33 -0400 Subject: [PATCH 2/3] Make schema specification "machine readable" Add directives to our custom-domain extension to declare objects and attributes, and use these to specify the schema. In addition to providing a slightly more structured way to specify attribute parameters, this also makes it possible to create a data model of the schema which we will eventually use to generate a JSON schema that can be used to partially validate CPS JSON. --- _extensions/cps.py | 253 +++++++- schema-supplement.rst | 151 +++-- schema.rst | 1294 +++++++++++++++++++---------------------- 3 files changed, 938 insertions(+), 760 deletions(-) diff --git a/_extensions/cps.py b/_extensions/cps.py index 75828f4..9235051 100644 --- a/_extensions/cps.py +++ b/_extensions/cps.py @@ -1,10 +1,28 @@ import re +from dataclasses import dataclass +from typing import cast from docutils import nodes -from docutils.parsers.rst import directives, roles +from docutils.parsers.rst import Directive, directives, roles from docutils.transforms import Transform from sphinx import addnodes, domains +from sphinx.util import logging +from sphinx.util.nodes import clean_astext + +logger = logging.getLogger(__name__) + +# ============================================================================= +def simplify_text(node): + paragraphs = [] + for p in clean_astext(node).split('\n\n'): + paragraphs.append(p.replace('\n', ' ')) + + return '\n\n'.join(paragraphs) + +# ============================================================================= +def make_code(text, classname): + return nodes.literal(text, text, classes=['code', classname]) # ============================================================================= class InternalizeLinks(Transform): @@ -55,6 +73,211 @@ def apply(self, **kwargs): # Replace the old node ref.replace_self(xref) +# ============================================================================= +class ObjectDirective(Directive): + required_arguments = 1 + optional_arguments = 0 + has_content = True + + # ------------------------------------------------------------------------- + def run(self): + name = self.arguments[0] + + # Create section node + section = nodes.section() + section.document = self.state.document + section.line = self.lineno + section['names'].append(f'{name}(object)') + + # Create text nodes for section title + title = [ + make_code(name, 'object'), + nodes.inline('(object)', '(object)', classes=['hidden']), + ] + section += nodes.title(name, '', *title) + + # Parse object description + content = nodes.Element() + self.state.nested_parse(self.content, self.content_offset, content) + section += content.children + + # Record section reference + self.state.document.note_implicit_target(section, section) + + # Record object on domain + env = self.state.document.settings.env + domain = cast(CpsDomain, env.get_domain('cps')) + domain.note_object(name, simplify_text(content)) + + # Return generated nodes + return [section] + +# ============================================================================= +class AttributeDirective(Directive): + required_arguments = 1 + optional_arguments = 0 + has_content = True + option_spec = { + 'type': directives.unchanged, + 'context': lambda a: a.split(), + 'overload': directives.flag, + 'required': directives.flag, + 'conditionally-required': directives.flag, + } + + # ------------------------------------------------------------------------- + def make_field(self, name, rawtext, body): + src, srcline = self.state.state_machine.get_source_and_line() + + field = nodes.field() + field.source = src + field.line = srcline + + field += nodes.field_name(name, name) + field += nodes.field_body(rawtext, *body) + + return field + + # ------------------------------------------------------------------------- + def make_list(self, values, nodetype, classes): + content = [nodetype(values[0], values[0], classes=classes)] + for value in values[1:]: + content += [ + nodes.Text(', '), + nodetype(value, value, classes=classes), + ] + return content + + # ------------------------------------------------------------------------- + def parse_type(self, typedesc): + if '|' in typedesc: + types = typedesc.split('|') + content = self.parse_type(types[0]) + for t in types[1:]: + content += [ + nodes.Text(' '), + nodes.inline('or', 'or', classes=['separator']), + nodes.Text(' '), + ] + self.parse_type(t) + + return content + + m = re.match(r'^(list|map)[(](.*)[)]$', typedesc) + if m: + outer, inner = m.groups() + content = [ + make_code(outer, 'type'), + nodes.Text(' of '), + ] + if outer == 'map': + content += [ + make_code('string', 'type'), + nodes.Text(' to '), + ] + + return content + self.parse_type(inner) + + elif typedesc in {'string'}: + return [make_code(typedesc, 'type')] + + else: + return [make_code(typedesc, 'object')] + + # ------------------------------------------------------------------------- + def run(self): + name = self.arguments[0] + typedesc = self.options['type'] + context = self.options['context'] + overload = 'overload' in self.options + required = 'required' in self.options + conditionally_required = 'conditionally-required' in self.options + + if overload: + target = f'{name} ({context[0]})' + else: + target = name + + if required: + required_text = 'Yes' + elif conditionally_required: + required_text = 'Special' + else: + required_text = 'No' + + # Create section node + section = nodes.section() + section.document = self.state.document + section.line = self.lineno + section['names'].append(target) + + # Create text nodes for section title + title = [make_code(name, 'attribute')] + if overload: + title += [ + nodes.Text(' '), + nodes.inline(f'({context[0]})', f'({context[0]})', + classes=['applies-to']), + ] + section += nodes.title(target, '', *title) + + # Create nodes for attribute information + fields = nodes.field_list() + fields += self.make_field( + 'Type', typedesc, self.parse_type(typedesc)) + fields += self.make_field( + 'Applies To', ', '.join(context), + self.make_list(context, nodes.literal, ['code', 'object'])) + fields += self.make_field( + 'Required', required_text, [nodes.Text(required_text)]) + section += fields + + # Parse attribute description + content = nodes.Element() + self.state.nested_parse(self.content, self.content_offset, content) + section += content.children + + # Record section reference + self.state.document.note_implicit_target(section, section) + + # Record object on domain + env = self.state.document.settings.env + domain = cast(CpsDomain, env.get_domain('cps')) + domain.note_attribute(name, context, typedesc, required=required, + description=simplify_text(content), node=section) + + # Return generated nodes + return [section] + +# ============================================================================= +@dataclass +class Attribute: + typedesc: str + description: str + required: bool + +# ============================================================================= +class AttributeSet: + + # ------------------------------------------------------------------------- + def __init__(self, name, context, attribute, node): + self.name = name + self.instances = [attribute] + self.context = {c: (0, node) for c in context} + + # ------------------------------------------------------------------------- + def overload(self, context, attribute, node): + i = len(self.instances) + self.instances.append(attribute) + for c in context: + if c in self.context: + logger.warning('duplicate declaration of attribute ' + f'{self.name!r} on object {c!r}', + location=node) + logger.warning(f'{self.name!r} was previously declared here', + location=self.context[c][1]) + else: + self.context[c] = (i, node) + # ============================================================================= class CpsDomain(domains.Domain): name = 'cps' @@ -63,6 +286,10 @@ class CpsDomain(domains.Domain): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + # Site-specific directives (used for JSON schema generation) + self.directives['object'] = ObjectDirective + self.directives['attribute'] = AttributeDirective + # Site-specific custom roles (these just apply styling) self.add_role('hidden') self.add_role('applies-to') @@ -80,6 +307,30 @@ def __init__(self, *args, **kwargs): self.add_code_role('var') self.add_code_role('env') + # ------------------------------------------------------------------------- + @property + def objects(self): + return self.data.setdefault('objects', {}) + + # ------------------------------------------------------------------------- + @property + def attributes(self): + return self.data.setdefault('attributes', {}) + + # ------------------------------------------------------------------------- + def note_object(self, name, description): + if name not in self.objects: + self.objects[name] = description + + # ------------------------------------------------------------------------- + def note_attribute(self, name, context, typedesc, + required, description, node): + a = Attribute(typedesc, description, required) + if name not in self.attributes: + self.attributes[name] = AttributeSet(name, context, a, node) + else: + self.attributes[name].overload(context, a, node) + # ------------------------------------------------------------------------- def add_role(self, name, styles=None, parent=roles.generic_custom_role): options = {} diff --git a/schema-supplement.rst b/schema-supplement.rst index 4865cdd..21dfdc2 100644 --- a/schema-supplement.rst +++ b/schema-supplement.rst @@ -20,85 +20,78 @@ Attributes\ :hidden:`(Supplemental)` By definition, none of the following attributes are required. -:attribute:`default_license` ----------------------------- - -:Type: :type:`string` -:Applies To: :object:`package` - -Specifies the `license`_ that is assumed to apply to a component, -if none is otherwise specified. -This is convenient for packages -that wish their `license`_ to reflect portions of the package -that are not reflected by a component (such as data files) -when most or all of the compiled artifacts use the same license. - -The value shall be a well formed -|SPDX|_ `License Expression`_ . - -:attribute:`description` ------------------------- - -:Type: :type:`string` -:Applies To: :object:`package`, :object:`component` - -Provides a human readable description of the function -which the package or component provides. - -:attribute:`display_name` -------------------------- - -:Type: :type:`string` -:Applies To: :object:`package` - -Provides a human readable name of the package. -If provided, tools may use this in informational messages -instead of, or in addition to, the canonical package name. - -:attribute:`license` --------------------- - -:Type: :type:`string` -:Applies To: :object:`package`, :object:`component` - -Specifies the license or licenses -under which the package is distributed. -The value shall be a well formed -|SPDX|_ `License Expression`_ . - -If parts of a package use different licenses, -this attribute may also be specified on a component -if doing so helps to clarifying the licensing. -(See also `default_license`_.) - -:attribute:`meta_comment` -------------------------- - -:Type: :type:`string` -:Applies To: :object:`package` - -Provides a description of the file contents, -for readers that may not be familiar with CPS files. -The typical value is -:string:`"Common Package Specification for "`. - -:attribute:`meta_schema` ------------------------- - -:Type: :type:`string` -:Applies To: :object:`package` - -Provides a URI link to a document describing the format of the CPS file. -The typical value is :string:`"https://cps-org.github.io/cps/"` -(i.e. the top level page of this site). - -:attribute:`website` --------------------- - -:Type: :type:`string` -:Applies To: :object:`package` - -Specifies the URI at which the package's website may be found. +.. ---------------------------------------------------------------------------- +.. cps:attribute:: default_license + :type: string + :context: package + + Specifies the `license`_ that is assumed to apply to a component, + if none is otherwise specified. + This is convenient for packages + that wish their `license`_ to reflect portions of the package + that are not reflected by a component (such as data files) + when most or all of the compiled artifacts use the same license. + + The value shall be a well formed + |SPDX|_ `License Expression`_ . + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: description + :type: string + :context: package component + + Provides a human readable description of the function + which the package or component provides. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: display_name + :type: string + :context: package + + Provides a human readable name of the package. + If provided, tools may use this in informational messages + instead of, or in addition to, the canonical package name. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: license + :type: string + :context: package component + + Specifies the license or licenses + under which the package is distributed. + The value shall be a well formed + |SPDX|_ `License Expression`_ . + + If parts of a package use different licenses, + this attribute may also be specified on a component + if doing so helps to clarifying the licensing. + (See also `default_license`_.) + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: meta_comment + :type: string + :context: package + + Provides a description of the file contents, + for readers that may not be familiar with CPS files. + The typical value is + :string:`"Common Package Specification for "`. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: meta_schema + :type: string + :context: package + + Provides a URI link to a document describing the format of the CPS file. + The typical value is :string:`"https://cps-org.github.io/cps/"` + (i.e. the top level page of this site). + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: website + :type: string + :context: package + + Specifies the URI at which the package's website may be found. .. ... .. ... .. ... .. ... .. ... .. ... .. ... .. ... .. ... .. ... .. ... .. diff --git a/schema.rst b/schema.rst index 8e56a06..3ff2561 100644 --- a/schema.rst +++ b/schema.rst @@ -4,34 +4,34 @@ Package Schema Objects ''''''' -:object:`package`\ :hidden:`(object)` -------------------------------------- +.. ---------------------------------------------------------------------------- +.. cps:object:: package -The root of a CPS document is a |package| object. -A |package| object describes a single package. + The root of a CPS document is a |package| object. + A |package| object describes a single package. -:object:`platform`\ :hidden:`(object)` --------------------------------------- +.. ---------------------------------------------------------------------------- +.. cps:object:: platform -A |platform| describes the platform -on which a package's components may run. + A |platform| describes the platform + on which a package's components may run. -:object:`requirement`\ :hidden:`(object)` ------------------------------------------ +.. ---------------------------------------------------------------------------- +.. cps:object:: requirement -A |requirement| describes the specifics of a package dependency. + A |requirement| describes the specifics of a package dependency. -:object:`component`\ :hidden:`(object)` ---------------------------------------- +.. ---------------------------------------------------------------------------- +.. cps:object:: component -A |component| is a consumable part of a package. -Typical components include libraries and executables. + A |component| is a consumable part of a package. + Typical components include libraries and executables. -:object:`configuration`\ :hidden:`(object)` -------------------------------------------- +.. ---------------------------------------------------------------------------- +.. cps:object:: configuration -A |configuration| holds attributes -that are specific to a particular configuration of a |component|. + A |configuration| holds attributes + that are specific to a particular configuration of a |component|. Attributes '''''''''' @@ -41,667 +41,601 @@ This shall be equivalent to omitting the attribute. Attribute names are case sensitive. -:attribute:`c_runtime_vendor` ------------------------------ - -:Type: |string| -:Applies To: |platform| -:Required: No - -Specifies that the package's CABI components -require the specified C standard/runtime library. -Typical (case-insensitive) values include -:string:`"bsd"` (libc), -:string:`"gnu"` (glibc), -:string:`"mingw"` and -:string:`"microsoft"`. - -:attribute:`c_runtime_version` ------------------------------- - -:Type: |string| -:Applies To: |platform| -:Required: No - -Specifies the minimum C standard/runtime library version -required by the package's CABI components. - -:attribute:`clr_vendor` ------------------------ - -:Type: |string| -:Applies To: |platform| -:Required: No - -Specifies that the package's CLR (.NET) components -require the specified `Common Language Runtime`_ vendor. -Typical (case-insensitive) values include -:string:`"microsoft"` and -:string:`"mono"`. - -:attribute:`clr_version` ------------------------- - -:Type: |string| -:Applies To: |platform| -:Required: No - -Specifies the minimum `Common Language Runtime`_ version -required to use the package's CLR (.NET) components. - -:attribute:`compat_version` ---------------------------- - -:Type: |string| -:Applies To: |package| -:Required: No - -Specifies the oldest version of the package -with which this version is compatible. -This information is used when a consumer requests a specific version. -If the version requested is equal to or newer -than the :attribute:`compat_version`, -the package may be used. - -If not specified, -the package is not compatible with previous versions -(i.e. :attribute:`compat_version` -is implicitly equal to :attribute:`version`). - -:attribute:`compile_features` ------------------------------ - -:Type: |string-list| -:Applies To: |component|, |configuration| -:Required: No - -Specifies a list of `Compiler Features`_ -that must be enabled or disabled -when compiling code that consumes the component. - -:attribute:`compile_flags` --------------------------- - -:Type: |language-string-list| -:Applies To: |component|, |configuration| -:Required: No - -Specifies a list of additional flags -that must be supplied to the compiler -when compiling code that consumes the component. -Note that compiler flags may not be portable; -use of this attribute is discouraged. - -A map may be used instead to give different values -depending on the language of the consuming source file. -Handling of such shall be the same as for `definitions`_. - -:attribute:`components` :applies-to:`(package)` ------------------------------------------------ - -:Type: |map| to |component| -:Applies To: |package| -:Required: Yes - -Specifies the components which the package provides. -Keys are the component names. - -:attribute:`components` :applies-to:`(requirement)` ---------------------------------------------------- - -:Type: |string-list| -:Applies To: |requirement| -:Required: No - -Specifies a list of components -which must be present in the required package -in order for the requirement to be satisfied. -Although the build tool will generally produce an error -if a consumer uses a component -which in turn requires a component that was not found, -early specification via this attribute -may help build tools to diagnose such issues earlier -and/or produce better diagnostics. - -This may also be used to specify dependencies -that are not expressed in component level dependencies, -such as a package's requirement -that a dependency includes a certain symbolic component, -or if a dependency is only expressed at run-time. - -:attribute:`configuration` --------------------------- - -:Type: |string| -:Applies To: |package| -:Required: Special - -Specifies the name of the configuration -described by a configuration-specific ``.cps`` -(see `Configuration Merging`_). -This attribute is required in a configuration-specific ``.cps``, -and shall be ignored otherwise. - -:attribute:`configurations` :applies-to:`(package)` ---------------------------------------------------- - -:Type: |string-list| -:Applies To: |package| -:Required: No - -Specifies the configurations that are preferred. -See `Package Configurations`_ for a description -of how configurations are used. - -:attribute:`configurations` :applies-to:`(component)` ------------------------------------------------------ - -:Type: |map| to |configuration| -:Applies To: |component| -:Required: No - -Specifies a set of configuration-specific attributes for a |component|. -Keys are the configuration names. - -:attribute:`cpp_runtime_vendor` -------------------------------- - -:Type: |string| -:Applies To: |platform| -:Required: No - -Specifies that the package's CABI components -require the specified C++ standard/runtime library. -Typical (case-insensitive) values include -:string:`"gnu"` (libstdc++), -:string:`"llvm"` (libc++) and -:string:`"microsoft"`. - -:attribute:`cpp_runtime_version` --------------------------------- - -:Type: |string| -:Applies To: |platform| -:Required: No - -Specifies the minimum C++ standard/runtime library version -required by the package's CABI components. - -:attribute:`cps_path` ---------------------- - -:Type: |string| -:Applies To: |package| -:Required: No - -Specifies the directory portion location of the ``.cps`` file. -This shall be an "absolute" path which starts with ``@prefix@``. -This provides an additional mechanism -by which the tool may deduce the package's prefix, -since the absolute location of the ``.cps`` file -will be known by the tool. -(See also `Prefix Determination`_.) - -:attribute:`cps_version` ------------------------- - -:Type: |string| -:Applies To: |package| -:Required: Yes - -Specifies the version of the CPS -to which this ``.cps`` file conforms. -This may be used by tools to provide backwards compatibility -in case of compatibility-breaking changes in the CPS. - -CPS version numbering follows |semver|_. -That is, tools that support CPS version ``.`` -are expected to be able to read files -with :attribute:`cps_version` ``.``, -even for Z > Y -(with the understanding that, in such cases, the tool -may miss non-critical information that the CPS provided). - -:attribute:`default_components` -------------------------------- - -:Type: |string-list| -:Applies To: |package| -:Required: No - -Specifies a list of components that should be inferred -if a consumer specifies a dependency on a package, -but not a specific component. - -:attribute:`definitions` ------------------------- - -:Type: |map| to |map| to |string| -:Applies To: |component|, |configuration| -:Required: No - -Specifies a collection of compile definitions that must be defined -when compiling code that consumes the component. -Each key in the inner map(s) is the name of a compile definition, -such that e.g. ``-Dkey=value`` is passed to the compiler. -A value may be |null|, indicating a definition with no value -(e.g. ``-Dkey`` is passed to the compiler). -Note that an *empty* string indicates ``-Dkey=``, -which may have a different effect than ``-Dkey``. - -The outer map is used to describe -language-specific definitions. -The build tool shall include -only those definitions -whose language matches (case-sensitive) -that of the (lower case) language -of the source file being compiled. -Recognized languages shall include -:string:`"c"`, -:string:`"cpp"`, and -:string:`"fortran"`. -Additionally, the value :string:`"*"` indicates -that the corresponding definitions apply to all languages. - -If a definition name is repeated -in both :string:`"*"` and a specific language, -the latter, when applicable to the source being compiled, -shall have precedence. - -:attribute:`hints` ------------------- - -:Type: |string-list| -:Applies To: |requirement| -:Required: No - -Specifies a list of paths -where a required dependency might be located. -When given, this will usually provide the location -of the dependency as it was consumed by the package -when the package was built, -so that consumers can easily find (correct) dependencies -if they are in a location that is not searched by default. - -:attribute:`includes` ---------------------- - -:Type: |language-string-list| -:Applies To: |component|, |configuration| -:Required: No - -Specifies a list of directories -which should be added to the include search path -when compiling code that consumes the component. -If a path starts with ``@prefix@``, -the package's install prefix is substituted -(see `Package Searching`_). -This is recommended, as it allows packages to be relocatable. - -A map may be used instead to give different values -depending on the language of the consuming source file. -Handling of such shall be the same as for `definitions`_. - -:attribute:`isa` ----------------- - -:Type: |string| -:Applies To: |platform| -:Required: No - -Specifies that the package's CABI components -require the specified `Instruction Set Architecture`_. -The value is case insensitive -and should follow the output of ``uname -m``. - -:attribute:`jvm_vendor` ------------------------ - -:Type: |string| -:Applies To: |platform| -:Required: No - -Specifies that the package's Java components -require the specified Java_ vendor. -Typical (case-insensitive) values include -:string:`"oracle"` and -:string:`"openjdk"`. - -:attribute:`jvm_version` ------------------------- - -:Type: |string| -:Applies To: |platform| -:Required: No - -Specifies the minimum Java_ Virtual Machine version -required to use the package's Java components. - -:attribute:`kernel` -------------------- - -:Type: |string| -:Applies To: |platform| -:Required: No - -Specifies the name of the operating system kernel -required by the package's components. -The value is case insensitive -and should follow the output of ``uname -s``. -Typical values include -:string:`"windows"`, -:string:`"cygwin"`, -:string:`"linux"` and -:string:`"darwin"`. - -:attribute:`kernel_version` ---------------------------- - -:Type: |string| -:Applies To: |platform| -:Required: No - -Specifies the minimum operating system kernel version -required by the package's components. - -:attribute:`link_features` --------------------------- - -:Type: |string-list| -:Applies To: |component|, |configuration| -:Required: No - -Specifies a list of `Linker Features`_ -that must be enabled or disabled -when linking code that consumes the component. - -:attribute:`link_flags` ------------------------ - -:Type: |string-list| -:Applies To: |component|, |configuration| -:Required: No - -Specifies a list of additional flags -that must be supplied to the linker -when linking code that consumes the component. -Note that linker flags may not be portable; -use of this attribute is discouraged. - -:attribute:`link_languages` ---------------------------- - -:Type: |string-list| -:Applies To: |component|, |configuration| -:Required: No - -Specifies the ABI language or languages of a static library -(`type`_ :string:`"archive"`). -Officially supported (case-insensitive) values are -:string:`"c"` (no special handling required) and -:string:`"cpp"` (consuming the static library -also requires linking against the C++ standard runtime). -The default is :string:`"c"`. - -:attribute:`link_libraries` ---------------------------- - -:Type: |string-list| -:Applies To: |component|, |configuration| -:Required: No - -Specifies a list of additional libraries that must be linked against -when linking code that consumes the component. -(Note that packages should avoid using this attribute if at all possible. -Use `requires (component)`_ instead whenever possible.) - -:attribute:`link_location` --------------------------- - -:Type: |string| -:Applies To: |component|, |configuration| -:Required: No - -Specifies an alternate location of the component -that should be used when linking against the component. -This attribute typically applies only to :string:`"dylib"` components -on platforms where the library is separated into multiple file components. -For example, on Windows, -this attribute shall give the location of the ``.lib``, -while `location`_ shall give the location of the ``.dll``. - -If the path starts with ``@prefix@``, -the package's install prefix is substituted -(see `Package Searching`_). -This is recommended, as it allows packages to be relocatable. - -:attribute:`link_requires` --------------------------- - -:Type: |string-list| -:Applies To: |component|, |configuration| -:Required: No - -Specifies additional components required by a component -which are needed only at the link stage. -Unlike `requires (component)`_, -only the required components' link dependencies -should be applied transitively; -additional properties such as compile and include attributes -of the required component(s) should be ignored. - -:attribute:`location` ---------------------- - -:Type: |string| -:Applies To: |component|, |configuration| -:Required: Depends - -Specifies the location of the component. -The exact meaning of this attribute -depends on the component type, -but typically it provides the path -to the component's primary artifact, -such as a ``.so`` or ``.jar``. -(For Windows DLL components, -this should be the location of the ``.dll``. -See also `link_location`_.) - -If the path starts with ``@prefix@``, -the package's install prefix is substituted -(see `Package Searching`_). -This is recommended, as it allows packages to be relocatable. - -This attribute is required for |component|\ s -that are not of :string:`"interface"` :attribute:`type`. - -:attribute:`name` ------------------ - -:Type: |string| -:Applies To: |package| -:Required: Yes - -Specifies the canonical name of the package. -In order for searching to succeed, -the name of the CPS file -without the ``.cps`` suffix -must exactly match (including case) -either :attribute:`name` as-is, -or :attribute:`name` converted to lower case. - -:attribute:`platform` ---------------------- - -:Type: |platform| -:Applies To: |package| -:Required: No - -Specifies the platform on which a package's components may run. -This allows tools to ignore packages -which target a different platform -than the platform that the consumer targets -(see `Package Searching`_). -Any platform attribute not specified -implies that the package's components -are agnostic to that platform attribute. -If this attribute is not specified, -the package is implied to be platform agnostic. -(This might be the case for a "library" -which consists entirely of C/C++ headers. -Note that JVM/CLR versions are platform attributes, -so packages consisting entirely of Java and/or CLR components -will still typically use this attribute.) - -:attribute:`requires` :applies-to:`(component)` ------------------------------------------------ - -:Type: |string-list| -:Applies To: |component|, |configuration| -:Required: No - -Specifies additional components required by a component. -This is used, for example, to indicate transitive dependencies. -Relative component names are interpreted relative to the current package. -Absolute component names must refer to a package required by this package -(see `requires (package)`_). -Compile and link attributes should be applied transitively, -as if the consuming component also directly consumed the components -required by the component being consumed. - -See also `link_requires`_. - -:attribute:`requires` :applies-to:`(package)` ---------------------------------------------- - -:Type: |map| to |requirement| -:Applies To: |package| -:Required: No - -Specifies additional packages that are required by this package. -Keys are the name of another required package. -Values are a valid |requirement| object or |null| -(equivalent to an empty |requirement| object) -describing the package required. - -:attribute:`type` ------------------ - -:Type: |string| (restricted) -:Applies To: |component| -:Required: Yes - -Specifies the type of a component. -The component type affects how the component may be used. -Officially supported values are :string:`"executable"` -(any artifact which the target platform can directly execute), -:string:`"archive"` (CABI static library), -:string:`"dylib"` (CABI shared library), -:string:`"module"` (CABI plugin library), -:string:`"jar"` (Java Archive), -:string:`"interface"` and :string:`"symbolic"`. -If the type is not recognized by the parser, -the component shall be ignored. -(Parsers are permitted to support additional types -as a conforming extension.) - -A :string:`"dylib"` is meant to be linked at compile time; -the :attribute:`location` specifies the artifact -required for such linking (i.e. the import library on PE platforms). -A :string:`"module"` is meant to be loaded at run time -with :code:`dlopen` or similar; -again, the :attribute:`location` specifies the appropriate artifact. - -An :string:`"interface"` component is a special case; -it may have the usual attributes of a component, -but does not have a location. -This can be used to create "virtual" components -that do not have an associated artifact. - -A :string:`"symbolic"` component is even more special, -as it has no (required) attributes at all, -and the meaning of any attributes or configurations -assigned to such a component is unspecified. -A :string:`"symbolic"` component is intended -to be used as a form of feature testing; -a package that has a feature that is meaningful to users -but does not otherwise map directly to a component -may use a symbolic component -to indicate availability of the feature to users. - -:attribute:`version` :applies-to:`(package)` --------------------------------------------- - -:Type: |string| -:Applies To: |package| -:Required: No - -Specifies the version of the package. -The format of this string is determined by `version_schema`_. - -If not provided, the CPS will not satisfy any request -for a specific version of the package. - -:attribute:`version` :applies-to:`(requirement)` ------------------------------------------------- - -:Type: |string| -:Applies To: |requirement| -:Required: No - -Specifies the required version of a package. -If omitted, any version of the required package is acceptable. -Semantics are the same -as for the :attribute:`version` attribute of a |package|. - -:attribute:`version_schema` ---------------------------- - -:Type: |string| -:Applies To: |package| -:Required: No - -Specifies the structure -to which the package's version numbering conforms. -Tools may use this to determine how to perform version comparisons. -Officially supported (case-insensitive) values are -:string:`"simple"` and :string:`"custom"` -(:string:`"rpm"` or :string:`"dpkg"` should be used where applicable, -but may not be supported by all tools). -If a package uses :string:`"custom"`, -version numbers may be compared, -but version ordering is not possible. -The default is :string:`"simple"`. - -Needless to say, -changing a package's version scheme between releases -is *very strongly discouraged*. - -Note that this attribute determines -only how version numbers are *ordered*. -It does not imply anything -about the compatibility or incompatibility -of various versions of a package. -See also `compat_version`_. - -- :string:`simple` - - The package's version number - shall match the regular expression - ``[0-9]+([.][0-9]+)*([-+].*)?``. - - The portion of the version - which precedes the optional ``-`` or ``+`` - may be interpreted as a tuple of integers, - in which leading zeros are ignored. - Version numbers are compared according to numerical order, - starting from the first (left-most) number of the tuples. - If two version numbers have different tuple sizes, - the shorter tuple shall be implicitly filled with zeros. - -.. deprecated:: 0.9.0 - - :string:`"semver"` is a deprecated alias for :string:`"simple"`. +.. ---------------------------------------------------------------------------- +.. cps:attribute:: c_runtime_vendor + :type: string + :context: platform + + Specifies that the package's CABI components + require the specified C standard/runtime library. + Typical (case-insensitive) values include + :string:`"bsd"` (libc), + :string:`"gnu"` (glibc), + :string:`"mingw"` and + :string:`"microsoft"`. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: c_runtime_version + :type: string + :context: platform + + Specifies the minimum C standard/runtime library version + required by the package's CABI components. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: clr_vendor + :type: string + :context: platform + + Specifies that the package's CLR (.NET) components + require the specified `Common Language Runtime`_ vendor. + Typical (case-insensitive) values include + :string:`"microsoft"` and + :string:`"mono"`. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: clr_version + :type: string + :context: platform + + Specifies the minimum `Common Language Runtime`_ version + required to use the package's CLR (.NET) components. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: compat_version + :type: string + :context: package + + Specifies the oldest version of the package + with which this version is compatible. + This information is used when a consumer requests a specific version. + If the version requested is equal to or newer + than the :attribute:`compat_version`, + the package may be used. + + If not specified, + the package is not compatible with previous versions + (i.e. :attribute:`compat_version` + is implicitly equal to :attribute:`version`). + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: compile_features + :type: list(string) + :context: component configuration + + Specifies a list of `Compiler Features`_ + that must be enabled or disabled + when compiling code that consumes the component. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: compile_flags + :type: list(string)|map(list(string)) + :context: component configuration + + Specifies a list of additional flags + that must be supplied to the compiler + when compiling code that consumes the component. + Note that compiler flags may not be portable; + use of this attribute is discouraged. + + A map may be used instead to give different values + depending on the language of the consuming source file. + Handling of such shall be the same as for `definitions`_. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: components + :type: map(component) + :context: package + :overload: + :required: + + Specifies the components which the package provides. + Keys are the component names. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: components + :type: list(string) + :context: requirement + :overload: + + Specifies a list of components + which must be present in the required package + in order for the requirement to be satisfied. + Although the build tool will generally produce an error + if a consumer uses a component + which in turn requires a component that was not found, + early specification via this attribute + may help build tools to diagnose such issues earlier + and/or produce better diagnostics. + + This may also be used to specify dependencies + that are not expressed in component level dependencies, + such as a package's requirement + that a dependency includes a certain symbolic component, + or if a dependency is only expressed at run-time. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: configuration + :type: string + :context: package + :conditionally-required: + + Specifies the name of the configuration + described by a configuration-specific ``.cps`` + (see `Configuration Merging`_). + This attribute is required in a configuration-specific ``.cps``, + and shall be ignored otherwise. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: configurations + :type: list(string) + :context: package + :overload: + + Specifies the configurations that are preferred. + See `Package Configurations`_ for a description + of how configurations are used. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: configurations + :type: map(configuration) + :context: component + :overload: + + Specifies a set of configuration-specific attributes for a |component|. + Keys are the configuration names. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: cpp_runtime_vendor + :type: string + :context: platform + + Specifies that the package's CABI components + require the specified C++ standard/runtime library. + Typical (case-insensitive) values include + :string:`"gnu"` (libstdc++), + :string:`"llvm"` (libc++) and + :string:`"microsoft"`. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: cpp_runtime_version + :type: string + :context: platform + + Specifies the minimum C++ standard/runtime library version + required by the package's CABI components. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: cps_path + :type: string + :context: package + + Specifies the directory portion location of the ``.cps`` file. + This shall be an "absolute" path which starts with ``@prefix@``. + This provides an additional mechanism + by which the tool may deduce the package's prefix, + since the absolute location of the ``.cps`` file + will be known by the tool. + (See also `Prefix Determination`_.) + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: cps_version + :type: string + :context: package + :required: + + Specifies the version of the CPS + to which this ``.cps`` file conforms. + This may be used by tools to provide backwards compatibility + in case of compatibility-breaking changes in the CPS. + + CPS version numbering follows |semver|_. + That is, tools that support CPS version ``.`` + are expected to be able to read files + with :attribute:`cps_version` ``.``, + even for Z > Y + (with the understanding that, in such cases, the tool + may miss non-critical information that the CPS provided). + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: default_components + :type: list(string) + :context: package + + Specifies a list of components that should be inferred + if a consumer specifies a dependency on a package, + but not a specific component. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: definitions + :type: map(map(string)) + :context: component configuration + + Specifies a collection of compile definitions that must be defined + when compiling code that consumes the component. + Each key in the inner map(s) is the name of a compile definition, + such that e.g. ``-Dkey=value`` is passed to the compiler. + A value may be |null|, indicating a definition with no value + (e.g. ``-Dkey`` is passed to the compiler). + Note that an *empty* string indicates ``-Dkey=``, + which may have a different effect than ``-Dkey``. + + The outer map is used to describe + language-specific definitions. + The build tool shall include + only those definitions + whose language matches (case-sensitive) + that of the (lower case) language + of the source file being compiled. + Recognized languages shall include + :string:`"c"`, + :string:`"cpp"`, and + :string:`"fortran"`. + Additionally, the value :string:`"*"` indicates + that the corresponding definitions apply to all languages. + + If a definition name is repeated + in both :string:`"*"` and a specific language, + the latter, when applicable to the source being compiled, + shall have precedence. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: hints + :type: list(string) + :context: requirement + + Specifies a list of paths + where a required dependency might be located. + When given, this will usually provide the location + of the dependency as it was consumed by the package + when the package was built, + so that consumers can easily find (correct) dependencies + if they are in a location that is not searched by default. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: includes + :type: list(string)|map(list(string)) + :context: component configuration + + Specifies a list of directories + which should be added to the include search path + when compiling code that consumes the component. + If a path starts with ``@prefix@``, + the package's install prefix is substituted + (see `Package Searching`_). + This is recommended, as it allows packages to be relocatable. + + A map may be used instead to give different values + depending on the language of the consuming source file. + Handling of such shall be the same as for `definitions`_. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: isa + :type: string + :context: platform + + Specifies that the package's CABI components + require the specified `Instruction Set Architecture`_. + The value is case insensitive + and should follow the output of ``uname -m``. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: jvm_vendor + :type: string + :context: platform + + Specifies that the package's Java components + require the specified Java_ vendor. + Typical (case-insensitive) values include + :string:`"oracle"` and + :string:`"openjdk"`. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: jvm_version + :type: string + :context: platform + + Specifies the minimum Java_ Virtual Machine version + required to use the package's Java components. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: kernel + :type: string + :context: platform + + Specifies the name of the operating system kernel + required by the package's components. + The value is case insensitive + and should follow the output of ``uname -s``. + Typical values include + :string:`"windows"`, + :string:`"cygwin"`, + :string:`"linux"` and + :string:`"darwin"`. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: kernel_version + :type: string + :context: platform + + Specifies the minimum operating system kernel version + required by the package's components. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: link_features + :type: list(string) + :context: component configuration + + Specifies a list of `Linker Features`_ + that must be enabled or disabled + when linking code that consumes the component. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: link_flags + :type: list(string) + :context: component configuration + + Specifies a list of additional flags + that must be supplied to the linker + when linking code that consumes the component. + Note that linker flags may not be portable; + use of this attribute is discouraged. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: link_languages + :type: list(string) + :context: component configuration + + Specifies the ABI language or languages of a static library + (`type`_ :string:`"archive"`). + Officially supported (case-insensitive) values are + :string:`"c"` (no special handling required) and + :string:`"cpp"` (consuming the static library + also requires linking against the C++ standard runtime). + The default is :string:`"c"`. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: link_libraries + :type: list(string) + :context: component configuration + + Specifies a list of additional libraries that must be linked against + when linking code that consumes the component. + (Note that packages should avoid using this attribute if at all possible. + Use `requires (component)`_ instead whenever possible.) + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: link_location + :type: string + :context: component configuration + + Specifies an alternate location of the component + that should be used when linking against the component. + This attribute typically applies only to :string:`"dylib"` components + on platforms where the library is separated into multiple file components. + For example, on Windows, + this attribute shall give the location of the ``.lib``, + while `location`_ shall give the location of the ``.dll``. + + If the path starts with ``@prefix@``, + the package's install prefix is substituted + (see `Package Searching`_). + This is recommended, as it allows packages to be relocatable. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: link_requires + :type: list(string) + :context: component configuration + + Specifies additional components required by a component + which are needed only at the link stage. + Unlike `requires (component)`_, + only the required components' link dependencies + should be applied transitively; + additional properties such as compile and include attributes + of the required component(s) should be ignored. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: location + :type: string + :context: component configuration + :conditionally-required: + + Specifies the location of the component. + The exact meaning of this attribute + depends on the component type, + but typically it provides the path + to the component's primary artifact, + such as a ``.so`` or ``.jar``. + (For Windows DLL components, + this should be the location of the ``.dll``. + See also `link_location`_.) + + If the path starts with ``@prefix@``, + the package's install prefix is substituted + (see `Package Searching`_). + This is recommended, as it allows packages to be relocatable. + + This attribute is required for |component|\ s + that are not of :string:`"interface"` :attribute:`type`. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: name + :type: string + :context: package + :required: + + Specifies the canonical name of the package. + In order for searching to succeed, + the name of the CPS file + without the ``.cps`` suffix + must exactly match (including case) + either :attribute:`name` as-is, + or :attribute:`name` converted to lower case. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: platform + :type: platform + :context: package + + Specifies the platform on which a package's components may run. + This allows tools to ignore packages + which target a different platform + than the platform that the consumer targets + (see `Package Searching`_). + Any platform attribute not specified + implies that the package's components + are agnostic to that platform attribute. + If this attribute is not specified, + the package is implied to be platform agnostic. + (This might be the case for a "library" + which consists entirely of C/C++ headers. + Note that JVM/CLR versions are platform attributes, + so packages consisting entirely of Java and/or CLR components + will still typically use this attribute.) + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: requires + :type: list(string) + :context: component configuration + :overload: + + Specifies additional components required by a component. + This is used, for example, to indicate transitive dependencies. + Relative component names are interpreted relative to the current package. + Absolute component names must refer to a package required by this package + (see `requires (package)`_). + Compile and link attributes should be applied transitively, + as if the consuming component also directly consumed the components + required by the component being consumed. + + See also `link_requires`_. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: requires + :type: map(requirement) + :context: package + :overload: + + Specifies additional packages that are required by this package. + Keys are the name of another required package. + Values are a valid |requirement| object or |null| + (equivalent to an empty |requirement| object) + describing the package required. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: type + :type: string + :context: component + :required: + + Specifies the type of a component. + The component type affects how the component may be used. + Officially supported values are :string:`"executable"` + (any artifact which the target platform can directly execute), + :string:`"archive"` (CABI static library), + :string:`"dylib"` (CABI shared library), + :string:`"module"` (CABI plugin library), + :string:`"jar"` (Java Archive), + :string:`"interface"` and :string:`"symbolic"`. + If the type is not recognized by the parser, + the component shall be ignored. + (Parsers are permitted to support additional types + as a conforming extension.) + + A :string:`"dylib"` is meant to be linked at compile time; + the :attribute:`location` specifies the artifact + required for such linking (i.e. the import library on PE platforms). + A :string:`"module"` is meant to be loaded at run time + with :code:`dlopen` or similar; + again, the :attribute:`location` specifies the appropriate artifact. + + An :string:`"interface"` component is a special case; + it may have the usual attributes of a component, + but does not have a location. + This can be used to create "virtual" components + that do not have an associated artifact. + + A :string:`"symbolic"` component is even more special, + as it has no (required) attributes at all, + and the meaning of any attributes or configurations + assigned to such a component is unspecified. + A :string:`"symbolic"` component is intended + to be used as a form of feature testing; + a package that has a feature that is meaningful to users + but does not otherwise map directly to a component + may use a symbolic component + to indicate availability of the feature to users. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: version + :type: string + :context: package + :overload: + + Specifies the version of the package. + The format of this string is determined by `version_schema`_. + + If not provided, the CPS will not satisfy any request + for a specific version of the package. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: version + :type: string + :context: requirement + :overload: + + Specifies the required version of a package. + If omitted, any version of the required package is acceptable. + Semantics are the same + as for the :attribute:`version` attribute of a |package|. + +.. ---------------------------------------------------------------------------- +.. cps:attribute:: version_schema + :type: string + :context: package + + Specifies the structure + to which the package's version numbering conforms. + Tools may use this to determine how to perform version comparisons. + Officially supported (case-insensitive) values are + :string:`"simple"` and :string:`"custom"` + (:string:`"rpm"` or :string:`"dpkg"` should be used where applicable, + but may not be supported by all tools). + If a package uses :string:`"custom"`, + version numbers may be compared, + but version ordering is not possible. + The default is :string:`"simple"`. + + Needless to say, + changing a package's version scheme between releases + is *very strongly discouraged*. + + Note that this attribute determines + only how version numbers are *ordered*. + It does not imply anything + about the compatibility or incompatibility + of various versions of a package. + See also `compat_version`_. + + - :string:`simple` + + The package's version number + shall match the regular expression + ``[0-9]+([.][0-9]+)*([-+].*)?``. + + The portion of the version + which precedes the optional ``-`` or ``+`` + may be interpreted as a tuple of integers, + in which leading zeros are ignored. + Version numbers are compared according to numerical order, + starting from the first (left-most) number of the tuples. + If two version numbers have different tuple sizes, + the shorter tuple shall be implicitly filled with zeros. + + .. deprecated:: 0.9.0 + + :string:`"semver"` is a deprecated alias for :string:`"simple"`. Notes ''''' From b2e7671d6a21b80e9f607fb5fce5cb23bc06cfc0 Mon Sep 17 00:00:00 2001 From: Matthew Woehlke Date: Fri, 3 May 2024 14:23:35 -0400 Subject: [PATCH 3/3] Generate JSON schema from documentation Using the structured information extracted from the documentation (see previous commit), build and write a JSON schema. This also introduces a new utility library to convert build the schema description from the set of objects and attributes. --- _extensions/cps.py | 47 +++++++++++++++++++++ _packages/jsb.py | 102 +++++++++++++++++++++++++++++++++++++++++++++ conf.py | 5 +++ pyproject.toml | 3 +- 4 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 _packages/jsb.py diff --git a/_extensions/cps.py b/_extensions/cps.py index 9235051..cdad821 100644 --- a/_extensions/cps.py +++ b/_extensions/cps.py @@ -1,3 +1,4 @@ +import os import re from dataclasses import dataclass from typing import cast @@ -6,6 +7,8 @@ from docutils.parsers.rst import Directive, directives, roles from docutils.transforms import Transform +from jsb import JsonSchema + from sphinx import addnodes, domains from sphinx.util import logging from sphinx.util.nodes import clean_astext @@ -346,9 +349,53 @@ def add_role(self, name, styles=None, parent=roles.generic_custom_role): def add_code_role(self, name, styles=None, parent=roles.code_role): self.add_role(name, styles, parent) + +# ============================================================================= +def write_schema(app, exception): + if exception is not None: + return + + config = app.env.config + title = f'{config.project} v{config.version}' + + domain = cast(CpsDomain, app.env.get_domain('cps')) + schema = JsonSchema(title, config.schema_id) + + object_attributes = {} + for attribute_set in domain.attributes.values(): + for i, attribute in enumerate(attribute_set.instances): + schema.add_attribute( + attribute_set.name, i, + attribute.typedesc, + attribute.description, + ) + + for context, attribute_ref in attribute_set.context.items(): + attribute = ( + attribute_set.name, + attribute_ref[0], + attribute_set.instances[attribute_ref[0]].required + ) + if context in object_attributes: + object_attributes[context].append(attribute) + else: + object_attributes[context] = [attribute] + + for name, description in domain.objects.items(): + schema.add_object_type(name, description, object_attributes[name]) + + output_path = os.path.join(app.outdir, config.schema_filename) + schema.write(config.schema_root_object, output_path) + # ============================================================================= def setup(app): app.add_domain(CpsDomain) + app.add_config_value('schema_id', '', '', [str]) + app.add_config_value('schema_filename', 'schema.json', '', [str]) + app.add_config_value('schema_root_object', None, '', [str]) # Add custom transform to resolve cross-file references app.add_transform(InternalizeLinks) + + # Add hook to write machine-readable schema description on completion + app.connect('build-finished', write_schema) diff --git a/_packages/jsb.py b/_packages/jsb.py new file mode 100644 index 0000000..91c9f85 --- /dev/null +++ b/_packages/jsb.py @@ -0,0 +1,102 @@ +import json +import re + +# ============================================================================= +class JsonSchema: + # ------------------------------------------------------------------------- + def __init__(self, title, uri): + self.meta = { + '$schema': 'http://json-schema.org/draft-07/schema', + '$id': uri, + 'title': title, + } + self.types = {} + self.object_types = {} + self.attributes = {} + + # ------------------------------------------------------------------------- + def add_type(self, typedesc): + if typedesc in self.types: + # Type already defined; nothing to do + return + + if '|' in typedesc: + types = typedesc.split('|') + for t in types: + self.add_type(t) + + self.types[typedesc] = { + 'oneOf': [ + {'$ref': f'#/definitions/types/{t}'} for t in types + ], + } + + return + + m = re.match(r'^(list|map)[(](.*)[)]$', typedesc) + if m: + outer, inner = m.groups() + self.add_type(inner) + + if outer == 'list': + self.types[typedesc] = { + 'type': 'array', + 'items': {'$ref': f'#/definitions/types/{inner}'}, + } + + else: + self.types[typedesc] = { + 'type': 'object', + 'patternProperties': { + '': {'$ref': f'#/definitions/types/{inner}'}, + }, + } + + elif typedesc in {'string'}: + # Handle simple (non-compound) types + self.types[typedesc] = {'type': typedesc} + + else: + # Anything unknown is assumed to be an object type, which must be + # defined via add_object_type + pass + + # ------------------------------------------------------------------------- + def add_attribute(self, name, instance, typedesc, description): + self.attributes[f'{name}@{instance}'] = { + 'description': description, + '$ref': f'#/definitions/types/{typedesc}', + } + + self.add_type(typedesc) + + # ------------------------------------------------------------------------- + def add_object_type(self, name, description, attributes, strict=False): + self.object_types[name] = { + 'type': 'object', + 'description': description, + 'properties': { + n: {'$ref': f'#/definitions/attributes/{n}@{i}'} + for n, i, r in attributes + }, + 'required': [n for n, i, r in attributes if r], + 'additionalProperties': not strict, + } + + # ------------------------------------------------------------------------- + def _build_schema(self, root_object): + all_types = self.types + all_types.update(self.object_types) + + schema = self.meta + schema['definitions'] = { + 'types': all_types, + 'attributes': self.attributes, + } + schema['$ref'] = f'#/definitions/types/{root_object}' + + return schema + + # ------------------------------------------------------------------------- + def write(self, root_object, path): + json.dump(self._build_schema(root_object), open(path, 'wt')) diff --git a/conf.py b/conf.py index fc43f6e..09cc38c 100644 --- a/conf.py +++ b/conf.py @@ -28,6 +28,11 @@ highlight_language = 'none' pygments_style = 'sphinx' +# -- Options for JSON schema output --------------------------------------- +schema_filename = 'cps.schema.json' +schema_root_object = 'package' +schema_id = 'https://cps-org.github.io/cps/' + schema_filename + # -- Options for HTML output ---------------------------------------------- html_style = 'cps.css' html_theme = 'default' diff --git a/pyproject.toml b/pyproject.toml index 2946648..4ee70cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,8 +9,9 @@ description = "" authors = ["The CPS Project <@>"] readme = "readme.md" packages = [ + { include = "jsb.py", from = "_packages" }, { include = "cps.py", from = "_extensions" }, - { include = "autosectionlabel.py", from = "_extensions" } + { include = "autosectionlabel.py", from = "_extensions" }, ] [tool.poetry.dependencies]