Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 223 additions & 0 deletions docs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,229 @@ def _merge_sourcelinks(name, sourcelinks, known_good = None):
tools = [merge_sourcelinks_tool],
)

def filtered_needs_json(
name,
src,
types = [],
components = [],
component_attr = "component",
visibility = None):
"""Extract a subset of sphinx-needs elements from a needs.json file.

Produces a `<name>.json` file containing only the needs that match all of
the given filters. This is useful to hand a downstream consumer just the
elements (e.g. `feat_req`) of one or more particular components.

Args:
name: Name of the generated target. The output file is `<name>.json`.
src: Label of a `needs_json` build output (a directory containing
`needs.json`), e.g. `":needs_json"` or `"@score_process//:needs_json"`.
types: Optional list of sphinx-needs element types to keep
(e.g. `["feat_req", "comp_req"]`). If empty, all types are kept.
components: Optional list of component names to keep. If empty, all
components are kept.
component_attr: Need attribute matched against `components`.
Defaults to `"component"`.
visibility: Standard Bazel visibility for the generated target.
"""
filter_tool = Label("//scripts_bazel:filter_needs_json")

type_args = " ".join(["--type '%s'" % t for t in types])
component_args = " ".join(["--component '%s'" % c for c in components])

native.genrule(
name = name,
srcs = [src],
outs = [name + ".json"],
cmd = """
$(location {filter_tool}) \
--output $@ \
--component-attr '{component_attr}' \
{type_args} \
{component_args} \
$(location {src})/needs.json
""".format(
filter_tool = filter_tool,
component_attr = component_attr,
type_args = type_args,
component_args = component_args,
src = src,
),
tools = [filter_tool],
visibility = visibility,
)

def component_requirements(
name,
src = "//:needs_json",
component = None,
visibility = None):
"""Extract the component requirements (`comp_req`) from a needs.json file.

Convenience wrapper around `filtered_needs_json`. Produces a `<name>.json`
file containing only `comp_req` elements.

Args:
name: Name of the generated target. The output file is `<name>.json`.
src: Label of a `needs_json` build output. Defaults to the calling
package's `//:needs_json`.
component: Optional component name. If given, only component requirements
tagged with that component are kept; if omitted, all component
requirements are kept.
visibility: Standard Bazel visibility for the generated target.
"""
filtered_needs_json(
name = name,
src = src,
types = ["comp_req"],
components = [component] if component else [],
component_attr = "tags",
visibility = visibility,
)

def feature_requirements(
name,
src = "//:needs_json",
feature = None,
visibility = None):
"""Extract the feature requirements (`feat_req`) from a needs.json file.

Convenience wrapper around `filtered_needs_json`. Produces a `<name>.json`
file containing only `feat_req` elements.

Args:
name: Name of the generated target. The output file is `<name>.json`.
src: Label of a `needs_json` build output. Defaults to the calling
package's `//:needs_json`.
feature: Optional feature name. If given, only feature requirements
tagged with that feature are kept; if omitted, all feature
requirements are kept.
visibility: Standard Bazel visibility for the generated target.
"""
filtered_needs_json(
name = name,
src = src,
types = ["feat_req"],
components = [feature] if feature else [],
component_attr = "tags",
visibility = visibility,
)

def assumptions_of_use(
name,
src = "//:needs_json",
component = None,
visibility = None):
"""Extract the assumptions of use (`aou_req`) from a needs.json file.

Convenience wrapper around `filtered_needs_json`. Produces a `<name>.json`
file containing only `aou_req` elements.

Args:
name: Name of the generated target. The output file is `<name>.json`.
src: Label of a `needs_json` build output. Defaults to the calling
package's `//:needs_json`.
component: Optional component name. If given, only assumptions of use
tagged with that component are kept; if omitted, all assumptions of
use are kept.
visibility: Standard Bazel visibility for the generated target.
"""
filtered_needs_json(
name = name,
src = src,
types = ["aou_req"],
components = [component] if component else [],
component_attr = "tags",
visibility = visibility,
)

def sphinx_needs_to_md(
name,
src,
title = "Sphinx-needs elements",
visibility = None):
"""Render the sphinx-needs elements of a needs.json file as a Markdown document.

Produces a `<name>.md` file containing a human readable description of every
sphinx-needs element found in `src`. Typically `src` is the output of a
`filtered_needs_json` target, but any `needs.json`-style file works.

Args:
name: Name of the generated target. The output file is `<name>.md`.
src: Label of a needs.json file, e.g. a `filtered_needs_json` target
(`":my_feat_reqs"`) or a `needs_json` directory output.
title: Title rendered at the top of the generated document.
visibility: Standard Bazel visibility for the generated target.
"""
sphinx_needs_to_md_tool = Label("//scripts_bazel:sphinx_needs_to_md")

native.genrule(
name = name,
srcs = [src],
outs = [name + ".md"],
cmd = """
$(location {sphinx_needs_to_md_tool}) \
--output $@ \
--title '{title}' \
$(location {src})
""".format(
sphinx_needs_to_md_tool = sphinx_needs_to_md_tool,
title = title,
src = src,
),
tools = [sphinx_needs_to_md_tool],
visibility = visibility,
)

def sphinx_needs_to_trlc(
name,
src,
package = "Needs",
visibility = None):
"""Convert the requirement sphinx-needs elements of a needs.json file into TRLC.

TRLC ("Treat Requirements Like Code",
https://github.com/bmw-software-engineering/trlc) is requirements-only tooling.
Only the S-CORE requirement element types are converted; everything else is
ignored:

* `feat_req` -> `ScoreReq.FeatReq` (feature requirement)
* `comp_req` -> `ScoreReq.CompReq` (component requirement)
* `aou_req` -> `ScoreReq.AoU` (assumption of use)

Produces a `<name>.trlc` data file in package `package` targeting the S-CORE
requirements metamodel (package `ScoreReq`) from
https://github.com/eclipse-score/tooling/tree/main/bazel/rules/rules_score/trlc.
Validate the output together with that metamodel (e.g. via `trlc_requirements`
using `score_requirements_model` as `spec`).

Args:
name: Name of the generated target. The output file is `<name>.trlc`.
src: Label of a needs.json file, e.g. a `filtered_needs_json` target
(`":my_feat_reqs"`) or a `needs_json` directory output.
package: TRLC package name used for the generated requirements.
visibility: Standard Bazel visibility for the generated target.
"""
sphinx_needs_to_trlc_tool = Label("//scripts_bazel:sphinx_needs_to_trlc")

native.genrule(
name = name,
srcs = [src],
outs = [name + ".trlc"],
cmd = """
$(location {sphinx_needs_to_trlc_tool}) \
--output $@ \
--package '{package}' \
$(location {src})
""".format(
sphinx_needs_to_trlc_tool = sphinx_needs_to_trlc_tool,
package = package,
src = src,
),
tools = [sphinx_needs_to_trlc_tool],
visibility = visibility,
)

def _missing_requirements(deps):
"""Add Python hub dependencies if they are missing."""
found = []
Expand Down
24 changes: 24 additions & 0 deletions scripts_bazel/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,27 @@ py_binary(
visibility = ["//visibility:public"],
deps = [],
)

py_binary(
name = "filter_needs_json",
srcs = ["filter_needs_json.py"],
main = "filter_needs_json.py",
visibility = ["//visibility:public"],
deps = [],
)

py_binary(
name = "sphinx_needs_to_md",
srcs = ["sphinx_needs_to_md.py"],
main = "sphinx_needs_to_md.py",
visibility = ["//visibility:public"],
deps = [],
)

py_binary(
name = "sphinx_needs_to_trlc",
srcs = ["sphinx_needs_to_trlc.py"],
main = "sphinx_needs_to_trlc.py",
visibility = ["//visibility:public"],
deps = [],
)
83 changes: 83 additions & 0 deletions scripts_bazel/README_needs_rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Sphinx-needs processing rules (playground)

> **WIP / demonstration only.** This branch (`ankr_rules_score_playground`) is a
> proof of concept to explore how `rules_score` could post-process sphinx-needs
> data. It is **not** production ready and is shared for demonstration purposes.

## Why these changes

The documentation build already produces a `needs.json` file that contains every
sphinx-needs element (requirements, assumptions of use, ...). Downstream tooling
often needs only a *subset* of that data, or the data in a *different format*.

This change adds a small set of Bazel macros (in [docs.bzl](../docs.bzl)) plus
the backing Python tools (in this folder) to:

1. **Filter** a `needs.json` down to selected element types and/or components.
2. **Render** the selected elements as a human readable Markdown document.
3. **Convert** S-CORE requirement elements into [TRLC](https://github.com/bmw-software-engineering/trlc)
data targeting the S-CORE requirements metamodel.

The goal is to show how requirements managed as sphinx-needs can be bridged to
other consumers (review docs, TRLC-based tooling) without manual copying.

## What was added

### Bazel macros (`docs.bzl`)

| Macro | Output | Purpose |
| --- | --- | --- |
| `filtered_needs_json` | `<name>.json` | Keep only needs matching the given `types` / `components`. |
| `component_requirements` | `<name>.json` | Convenience wrapper for `comp_req` elements. |
| `feature_requirements` | `<name>.json` | Convenience wrapper for `feat_req` elements. |
| `assumptions_of_use` | `<name>.json` | Convenience wrapper for `aou_req` elements. |
| `sphinx_needs_to_md` | `<name>.md` | Render needs as a Markdown document. |
| `sphinx_needs_to_trlc` | `<name>.trlc` | Convert S-CORE requirements to TRLC. |

### Python tools (`scripts_bazel/`)

- [filter_needs_json.py](filter_needs_json.py) — extract a subset of needs.
- [sphinx_needs_to_md.py](sphinx_needs_to_md.py) — render needs as Markdown.
- [sphinx_needs_to_trlc.py](sphinx_needs_to_trlc.py) — convert needs to TRLC.

The matching `py_binary` targets are declared in [BUILD](BUILD).

## How to use

In a `BUILD` file that already has a `needs_json` target, load the macros and
chain them:

```starlark
load("@docs-as-code//:docs.bzl", "feature_requirements", "sphinx_needs_to_md", "sphinx_needs_to_trlc")

# 1. Filter: keep only the feature requirements of one feature.
feature_requirements(
name = "my_feature_reqs",
src = "//:needs_json",
feature = "my_feature",
)

# 2. Render the filtered set as Markdown.
sphinx_needs_to_md(
name = "my_feature_reqs_md",
src = ":my_feature_reqs",
title = "My feature requirements",
)

# 3. Convert the filtered set to TRLC.
sphinx_needs_to_trlc(
name = "my_feature_reqs_trlc",
src = ":my_feature_reqs",
package = "MyFeature",
)
```

Build any of the targets to produce the corresponding output file:

```bash
bazel build //path/to:my_feature_reqs_md
bazel build //path/to:my_feature_reqs_trlc
```

You can also call `filtered_needs_json` directly for full control over the
`types`, `components`, and `component_attr` filters.
Loading
Loading