Skip to content

Commit

Permalink
✨ Add needs_schema to needs.json (#1230)
Browse files Browse the repository at this point in the history
This PR builds and adds a schema for needs in the `needs.json`, under the `needs_schema` key (one per version).
This will allow for people/tools to understand what each need field actually is, and also for a follow up PR to remove defaults.
  • Loading branch information
chrisjsewell authored Aug 21, 2024
1 parent aa4b068 commit 4641f88
Show file tree
Hide file tree
Showing 11 changed files with 3,699 additions and 47 deletions.
136 changes: 92 additions & 44 deletions docs/builders.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ This allows to export specified filter results only.
Format
++++++

As well as the ``filters`` and ``needs`` data, the **needs.json** file also contains the ``needs_schema``.
This is a JSON schema of for the data structure of a single need,
and also includes a ``field_type`` for each field, to denote the source of the field,
and can be one of: ``core``, ``links``, ``extra``, ``global`` ``bool``.

.. code-block:: python
{
Expand All @@ -71,65 +76,108 @@ Format
"1.0": {
"created": "2017-07-03T11:54:42.433868",
"filters": {
"FILTER_1": {
"amount": 1,
"export_id": "FILTER_1",
"filter": "",
"result": [
"IMPL_01",
],
"status": [],
"tags": "",
"types": []
"FILTER_1": {
"amount": 1,
"export_id": "FILTER_1",
"filter": "",
"result": ["IMPL_01"],
"status": [],
"tags": "",
"types": []
},
"needs_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"description": "ID of the data.",
"field_type": "core",
"type": "string"
},
"type": {
"description": "Type of the need.",
"field_type": "core",
"type": "string"
},
"links": {
"description": "Link field",
"field_type": "links",
"items": {
"type": "string"
},
"type": "array"
},
"status": {
"description": "Status of the need.",
"field_type": "core",
"type": [
"string",
"null"
]
},
...
}
},
"needs": {
"IMPL_01": {
"description": "Incoming links of this spec: :need_incoming:`IMPL_01`.",
"id": "IMPL_01",
"links": [
"OWN_ID_123"
],
"sections": [
"Examples"
],
"status": null,
"tags": [],
"title": "Implementation for specification",
"type": "impl",
"type_name": "Implementation"
}
"links": ["OWN_ID_123"],
"status": null,
...
},
...
}
},
"1.5": {
"created": "2017-07-03T16:10:31.633425",
"filters": {
"FILTER_1": {
"amount": 1,
"export_id": "FILTER_1",
"filter": "",
"result": [
"IMPL_01",
],
"status": [],
"tags": "",
"types": []
"FILTER_1": {
"amount": 1,
"export_id": "FILTER_1",
"filter": "",
"result": ["IMPL_01"],
"status": [],
"tags": "",
"types": []
},
"needs_schema": {
"id": {
"description": "ID of the data.",
"field_type": "core",
"type": "string"
},
"type": {
"description": "Type of the need.",
"field_type": "core",
"type": "string"
},
"links": {
"description": "Link field",
"field_type": "links",
"items": {
"type": "string"
},
"type": "array"
},
"status": {
"description": "Status of the need.",
"field_type": "core",
"type": [
"string",
"null"
]
},
...
},
"needs": {
"IMPL_01": {
"description": "Incoming links",
"id": "IMPL_01",
"links": [
"OWN_ID_123"
],
"sections": [
"Examples"
],
"status": "closed",
"tags": ["links","update"],
"title": "Implementation for specification",
"type": "impl",
"type_name": "Implementation"
}
"links": ["OWN_ID_123"],
"status": "closed",
...
},
...
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion sphinx_needs/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,9 @@ def finish(self) -> None:
if not os.path.exists(needs_dir):
os.makedirs(needs_dir, exist_ok=True)
for need in filtered_needs:
needs_list = NeedsList(self.env.config, self.outdir, self.srcdir)
needs_list = NeedsList(
self.env.config, self.outdir, self.srcdir, add_schema=False
)
needs_list.wipe_version(version)
needs_list.add_need(version, need)
id = need["id"]
Expand Down
65 changes: 63 additions & 2 deletions sphinx_needs/needsfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,67 @@
from jsonschema import Draft7Validator
from sphinx.config import Config

from sphinx_needs.config import NeedsSphinxConfig
from sphinx_needs.config import NEEDS_CONFIG, NeedsSphinxConfig
from sphinx_needs.data import NeedsCoreFields, NeedsFilterType, NeedsInfoType
from sphinx_needs.logging import get_logger

log = get_logger(__name__)


def generate_needs_schema(config: Config) -> dict[str, Any]:
"""Generate a JSON schema for all fields in each need item.
It is based on:
* the core fields defined in NeedsCoreFields
* the extra options defined dynamically
* the global options defined dynamically
* the extra links defined dynamically
"""
properties: dict[str, Any] = {}

for name, extra_params in NEEDS_CONFIG.extra_options.items():
properties[name] = {
"type": "string",
"description": extra_params.description,
"field_type": "extra",
}

# TODO currently extra options can overlap with core fields,
# in which case they are ignored,
# (this is the case for `type` added by the github service)
# hence this is why we add the core options after the extra options
for name, core_params in NeedsCoreFields.items():
if core_params.get("exclude_json"):
continue
properties[name] = core_params["schema"]
properties[name]["description"] = f"{core_params['description']}"
properties[name]["field_type"] = "core"

needs_config = NeedsSphinxConfig(config)

for link in needs_config.extra_links:
properties[link["option"]] = {
"type": "array",
"items": {"type": "string"},
"description": "Link field",
"field_type": "links",
}

for name in needs_config.global_options:
if name not in properties:
properties[name] = {
"type": "string",
"description": "Added by needs_global_options configuration",
"field_type": "global",
}

return {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": properties,
}


class NeedsList:
JSON_KEY_EXCLUSIONS_NEEDS = {
name for name, params in NeedsCoreFields.items() if params.get("exclude_json")
Expand All @@ -42,11 +96,14 @@ class NeedsList:
"content_node",
}

def __init__(self, config: Config, outdir: str, confdir: str) -> None:
def __init__(
self, config: Config, outdir: str, confdir: str, add_schema: bool = True
) -> None:
self.config = config
self.needs_config = NeedsSphinxConfig(config)
self.outdir = outdir
self.confdir = confdir
self._add_schema = add_schema
self.current_version = config.version
self.project = config.project
self.needs_list = {
Expand All @@ -70,6 +127,10 @@ def update_or_add_version(self, version: str) -> None:
"filters_amount": 0,
"filters": {},
}
if self._add_schema:
self.needs_list["versions"][version]["needs_schema"] = (
generate_needs_schema(self.config)
)
if not self.needs_config.reproducible_json:
self.needs_list["versions"][version]["created"] = ""

Expand Down
Loading

0 comments on commit 4641f88

Please sign in to comment.