From 643c0e65403866ca5300dc6494fa7b7cacaa87b7 Mon Sep 17 00:00:00 2001 From: enn35999 Date: Fri, 30 Oct 2026 17:46:33 +0100 Subject: [PATCH 1/2] Release 0.5.0: canonical passport model, DPP4Fun profile, JSON clients, and reworked documentation --- .github/workflows/ci.yml | 2 +- .github/workflows/mock-services-demo.yml | 4 +- CHANGELOG.md | 117 +- CONTRIBUTING.md | 130 + README.md | 465 +- RELEASING.md | 83 +- .../python-client-request-flow.mmd | 10 +- .../python-client-request-flow.svg | 2 +- docs/architecture/python-core-model.mmd | 13 +- docs/architecture/python-core-model.svg | 2 +- docs/architecture/python-dpp4fun-model.mmd | 13 +- docs/architecture/python-dpp4fun-model.svg | 2 +- docs/architecture/python-sdk-overview.mmd | 30 +- docs/architecture/python-sdk-overview.svg | 2 +- docs/clients-guide.md | 173 + docs/conformance.md | 51 + docs/diagram-evidence.md | 46 - docs/dpp4fun-fields.md | 667 + docs/extending.md | 171 + docs/model-guide.md | 438 +- docs/overview.md | 44 - docs/usage.md | 212 - docs/validation-guide.md | 59 - docs/validation-rules.md | 50 - examples/consumer_lifecycle.py | 134 + examples/mock-services-demo/.env.example | 4 +- .../mock-services-demo/ADVANCED_OPERATIONS.md | 263 +- examples/mock-services-demo/README.md | 217 +- examples/mock-services-demo/compose.yaml | 4 +- examples/mock-services-demo/env/0.5.1.env | 2 +- examples/mock-services-demo/pyproject.toml | 2 +- .../controlled_scenarios.py | 291 +- .../src/dpp_mock_services_demo/fixtures.py | 320 +- .../integration_scenarios.py | 1046 +- .../src/dpp_mock_services_demo/readiness.py | 72 +- .../registry_scenarios.py | 275 +- .../src/dpp_mock_services_demo/reporting.py | 14 +- .../repository_scenarios.py | 525 +- .../dpp_mock_services_demo/sdk_scenarios.py | 1240 +- examples/mock-services-demo/tests/test_cli.py | 6 +- .../tests/test_compose_config.py | 4 +- .../mock-services-demo/tests/test_fixtures.py | 123 +- .../tests/test_installed_sdk_command.py | 2 +- .../tests/test_integration_scenarios.py | 566 +- .../tests/test_manage_services_script.py | 13 +- .../tests/test_mock_services_integration.py | 16 +- .../tests/test_registry_scenarios.py | 139 +- .../tests/test_reporting.py | 6 +- .../tests/test_repository_scenarios.py | 241 +- .../tests/test_sdk_output.py | 8 +- .../tests/test_sdk_scenarios.py | 80 +- .../verification/test_live_contract_matrix.py | 2 +- .../tests/verification/test_maintainer.py | 4 +- .../test_verification_inventory.py | 4 +- scripts/generate_dpp4fun_fields.py | 129 + scripts/render_diagrams.ps1 | 13 +- src/dpp_sdk/__init__.py | 130 +- src/dpp_sdk/clients/README.md | 321 +- src/dpp_sdk/clients/__init__.py | 28 +- src/dpp_sdk/clients/_http.py | 174 +- src/dpp_sdk/clients/endpoints.py | 42 - src/dpp_sdk/clients/errors.py | 20 +- src/dpp_sdk/clients/payloads.py | 150 +- src/dpp_sdk/clients/registry.py | 75 +- src/dpp_sdk/clients/repo.py | 339 +- src/dpp_sdk/core/README.md | 139 +- src/dpp_sdk/core/__init__.py | 90 +- src/dpp_sdk/core/json_codec.py | 987 + src/dpp_sdk/core/model.py | 478 +- src/dpp_sdk/core/semantic.py | 69 + src/dpp_sdk/core/validation.py | 1480 +- src/dpp_sdk/dpp4fun/README.md | 140 +- src/dpp_sdk/dpp4fun/__init__.py | 147 +- src/dpp_sdk/dpp4fun/characteristics.py | 55 + src/dpp_sdk/dpp4fun/composites.py | 113 + src/dpp_sdk/dpp4fun/model.py | 147 - src/dpp_sdk/dpp4fun/profile.py | 189 + .../semantic/dpp4fun-semantic-profile-v1.json | 25507 ++++++++++++++++ src/dpp_sdk/dpp4fun/transport.py | 143 +- src/dpp_sdk/dpp4fun/typed_model.py | 511 + src/dpp_sdk/dpp4fun/units.py | 108 + src/dpp_sdk/dpp4fun/validation.py | 700 +- tests/conftest.py | 85 - tests/fixtures/java-contract/README.md | 12 - .../java-contract/canonical-java-output.json | 1 - .../java-contract/fixture-metadata.json | 161 - .../invalid-cross-object-rule.json | 1 - .../invalid-missing-required-field.json | 1 - .../java-contract/invalid-semantic-rule.json | 1 - .../legacy-blocker-placeholders.json | 15 - .../fixtures/java-contract/malformed-json.txt | 1 - .../java-contract/valid-complete-dpp.json | 1 - .../java-contract/valid-flat-input.json | 1 - .../java-contract/valid-minimal-dpp.json | 1 - .../java-contract/valid-nested-input.json | 1 - tests/installed_wheel_integration.py | 219 +- tests/test_clients.py | 1428 +- tests/test_core_json_codec.py | 287 + tests/test_core_model.py | 518 +- tests/test_core_validation.py | 484 - tests/test_core_validation_parity.py | 692 + tests/test_documentation.py | 272 +- tests/test_dpp4fun_final_parity.py | 463 + tests/test_dpp4fun_model.py | 301 - tests/test_dpp4fun_typed_profile.py | 418 + tests/test_dpp4fun_validation.py | 516 +- tests/test_end_to_end.py | 98 +- tests/test_integration_live.py | 143 +- tests/test_java_contract_fixtures.py | 82 - tests/test_live_test_gating.py | 15 +- tests/test_python_only_extensions.py | 180 +- tests/test_readme_examples.py | 316 +- tests/test_release_validation.py | 55 +- tests/test_smoke.py | 130 +- tests/test_transport_roundtrip.py | 292 +- 115 files changed, 37921 insertions(+), 10103 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 docs/clients-guide.md create mode 100644 docs/conformance.md delete mode 100644 docs/diagram-evidence.md create mode 100644 docs/dpp4fun-fields.md create mode 100644 docs/extending.md delete mode 100644 docs/overview.md delete mode 100644 docs/usage.md delete mode 100644 docs/validation-guide.md delete mode 100644 docs/validation-rules.md create mode 100644 examples/consumer_lifecycle.py create mode 100644 scripts/generate_dpp4fun_fields.py delete mode 100644 src/dpp_sdk/clients/endpoints.py create mode 100644 src/dpp_sdk/core/json_codec.py create mode 100644 src/dpp_sdk/core/semantic.py create mode 100644 src/dpp_sdk/dpp4fun/characteristics.py create mode 100644 src/dpp_sdk/dpp4fun/composites.py delete mode 100644 src/dpp_sdk/dpp4fun/model.py create mode 100644 src/dpp_sdk/dpp4fun/profile.py create mode 100644 src/dpp_sdk/dpp4fun/semantic/dpp4fun-semantic-profile-v1.json create mode 100644 src/dpp_sdk/dpp4fun/typed_model.py create mode 100644 src/dpp_sdk/dpp4fun/units.py delete mode 100644 tests/fixtures/java-contract/README.md delete mode 100644 tests/fixtures/java-contract/canonical-java-output.json delete mode 100644 tests/fixtures/java-contract/fixture-metadata.json delete mode 100644 tests/fixtures/java-contract/invalid-cross-object-rule.json delete mode 100644 tests/fixtures/java-contract/invalid-missing-required-field.json delete mode 100644 tests/fixtures/java-contract/invalid-semantic-rule.json delete mode 100644 tests/fixtures/java-contract/legacy-blocker-placeholders.json delete mode 100644 tests/fixtures/java-contract/malformed-json.txt delete mode 100644 tests/fixtures/java-contract/valid-complete-dpp.json delete mode 100644 tests/fixtures/java-contract/valid-flat-input.json delete mode 100644 tests/fixtures/java-contract/valid-minimal-dpp.json delete mode 100644 tests/fixtures/java-contract/valid-nested-input.json create mode 100644 tests/test_core_json_codec.py delete mode 100644 tests/test_core_validation.py create mode 100644 tests/test_core_validation_parity.py create mode 100644 tests/test_dpp4fun_final_parity.py delete mode 100644 tests/test_dpp4fun_model.py create mode 100644 tests/test_dpp4fun_typed_profile.py delete mode 100644 tests/test_java_contract_fixtures.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb15db3..7138e53 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [main] + branches: [main, feat/final-java-parity-migration] pull_request: branches: [main] diff --git a/.github/workflows/mock-services-demo.yml b/.github/workflows/mock-services-demo.yml index 2691ef2..f7fb3d5 100644 --- a/.github/workflows/mock-services-demo.yml +++ b/.github/workflows/mock-services-demo.yml @@ -39,7 +39,7 @@ jobs: python -m build "$DEMO_DIR" --outdir "$DEMO_DIR/dist" python -m venv .mock-services-demo-venv .mock-services-demo-venv/bin/python -m pip install \ - dist/dpp_sdk-0.4.0-py3-none-any.whl \ + dist/dpp_sdk-0.5.0-py3-none-any.whl \ "$DEMO_DIR"/dist/dpp_sdk_mock_services_demo-0.1.0-py3-none-any.whl .mock-services-demo-venv/bin/python -m pip check @@ -58,7 +58,7 @@ jobs: run: | mkdir -p artifacts outside="$(mktemp -d)" - sdk_wheel="$GITHUB_WORKSPACE/dist/dpp_sdk-0.4.0-py3-none-any.whl" + sdk_wheel="$GITHUB_WORKSPACE/dist/dpp_sdk-0.5.0-py3-none-any.whl" report="$GITHUB_WORKSPACE/artifacts/verification-report-${{ inputs.profile }}.json" set +e if [ "${{ inputs.profile }}" = "0.4.0" ]; then diff --git a/CHANGELOG.md b/CHANGELOG.md index 328205c..5e4f50a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,105 @@ All notable changes to `dpp-sdk` are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.4.0] - Synchronisation with java-dpp-sdk +## [0.5.0] - 2026-10-30 + + +### Added +- A canonical `DigitalProductPassport` model with a `DppHeader` and typed DataElement variants, + plus FULL and COMPRESSED JSON codecs and explicit semantic, format, and identifier validation. +- A typed DPP4Fun profile over the core passport, including its bundled 488-definition semantic catalog, + typed aspects and Characteristics variants, reusable composites, profile validation, and transport codec. +- Generic JSON repository and registry clients that accept JSON-compatible request values and return + response envelopes without owning domain codecs or validation. + +### Changed +- The core model and furniture profile now use the final passport structure and current profile + semantics; the earlier `Dpp`/`DppCore` and flat furniture model shapes are no longer accepted as + current model APIs. +- Client methods return neutral response DTOs. Repository payloads are exposed through + `DppApiResponse.payload`; clients do not parse them into caller-selected model types. +- Public model and client attributes use Python `snake_case` while JSON aliases retain the API's + canonical field names. +- Repository and registry clients require explicit service base URLs. Callers own model parsing, + serialization, and validation. + +### Fixed +- Reject malformed HTTP base URLs before sending a request, and require `messageType` and `text` + for each API message while allowing the outer `message` list to be absent or empty. +- Preserve numeric DataElement equality and hashing across nested integer, float, and Decimal values. +- Apply the approved DPP4Fun profile bindings, unit handling, and semantic-resolution behavior. + +### Documentation +- Added a generated DPP4Fun field reference, a guide for building your own profile on Core, a + standards and validation-rules page, and a contributing guide. +- Reorganized the model guide around tasks (build, read, edit, validate, convert) and documented + the read API (`typed_aspect()`, `field()`, `quantity()`, `selected_characteristics()`). +- The README quickstart now reads the passport back as a typed object and lists the exceptions. + +### Breaking changes and migration + +The released `v0.4.0` repository client accepted a model codec and validator and returned typed model +objects. The `0.5.0` client takes only a service URL (and optionally a caller-supplied HTTPX client), +accepts JSON-compatible values, and returns response envelopes. Move validation and codec work to the +application boundary: + +```python +# v0.4.0 +repo = DppRepoClient(base_url, codec, validate_dpp4fun) +created = repo.create_dpp(passport) +loaded = repo.read_dpp_by_id(created.dppId) + +# 0.5.0 +import json +from dpp_sdk import Dpp4FunJsonCodec, validate_dpp4fun +from dpp_sdk.clients import DppRepresentation, DppRepoClient + +codec = Dpp4FunJsonCodec() +validation = validate_dpp4fun(passport) +if not validation.is_valid: + raise ValueError(validation.issues) +with DppRepoClient(base_url) as repo: + created = repo.create_dpp(json.loads(codec.to_json(passport))) + if created.digital_product_passport_id is None: + raise RuntimeError("repository did not return a passport ID") + response = repo.read_dpp_by_id( + created.digital_product_passport_id, + DppRepresentation.FULL, + ) + loaded = codec.from_json(json.dumps(response.payload)) +``` + +`DppApiResponse` carries `status_code`, `payload`, and `message`. API failures raise +`DppApiClientError`; malformed JSON or payload mapping raises `DppMappingClientError`; unstructured +HTTP failures and transport failures use `DppHttpClientError` and `DppNetworkClientError`. Semantic +validation is performed explicitly before a request with the relevant core or DPP4Fun validator. + +The former `Dpp`/`DppCore` model family and flat furniture classes were replaced by +`DigitalProductPassport`, `DppHeader`, five DataElement variants, and a DPP4Fun aggregate over the +canonical passport. Python object attributes also changed from camelCase to `snake_case`; for +example, `dpp.passportMetadata.uniqueProductIdentifier` is now +`passport.dpp_header.unique_product_identifier`, while the JSON key remains +`uniqueProductIdentifier`. Map stored legacy fields to the current model explicitly; no general +legacy model converter is provided. Use `DppJsonCodec.to_expanded_json()` or +`DppJsonCodec.to_compressed_json()` for core transport, and `Dpp4FunJsonCodec` for typed profile +views. FULL responses can be decoded with the codec; COMPRESSED responses may need prior metadata or +semantic resolution to restore typed details. + +The `for_local_mock()` factories, default endpoint helpers/constants, `health_check()`, typed +`read_compressed_dpp_by_id()`, product-ID historical-read helper, the `dpp_sdk.clients.DppCodec` +and `dpp_sdk.clients.DppValidator` typing protocols, `DppValidationClientError`, and the +`UpdateDataElementRequest` compatibility DTO are not part of the current client API. (The model +validator `dpp_sdk.core.DppValidator` is unaffected.) Pass local service URLs directly, check service health independently, use +`read_dpp_version_by_id_and_date()` for supported history reads, and pass direct JSON-compatible +DataElement bodies to fine-grained update methods. + +### Compatibility +- The generic JSON client API is a breaking change from the released typed-client API; see the + migration examples above. +- The legacy product-ID history route and legacy registry request aliases are not part of the + current public Python client surface. + +## [0.4.0] - 2026-08-11 ### Added - An isolated `examples/mock-services-demo` consumer project with reproducible Mock image @@ -46,8 +144,6 @@ All notable changes to `dpp-sdk` are documented here. The format is based on boundary, while direct defensive validators retain indexed semantic errors. ### Changed -- Active model guidance and contract tests consistently use `with_updates()` as the - revalidating immutable-edit seam; intentional Pydantic bypass probes remain internal. - `README.md`: full lifecycle Quickstart (build → validate → store → register → read → update → delete) now uses application-provided endpoints and no-network examples. - Domain models expose `with_updates()` for revalidated immutable updates; contracted collections @@ -71,9 +167,8 @@ All notable changes to `dpp-sdk` are documented here. The format is based on ## [0.2.1] — Previous package baseline -- Package metadata and `dpp_sdk.__version__` identified the preceding checkout as `0.2.1`. -- This repository checkout contains no local release tag for `0.2.1`; commit history remains - the provenance source for changes predating the current unreleased section. +- Package metadata and `dpp_sdk.__version__` identified version `0.2.1`. +- Commit history remains the provenance source for changes predating the `0.2.1` baseline. ## [0.1.1] @@ -90,7 +185,9 @@ First public release of the Python port of the Mock DPP SDK. - `dpp_sdk.clients` — `httpx`-based HTTP clients for the DPP repository and registry APIs. - Packaging: PEP 621 `pyproject.toml`, `src/` layout, `py.typed` marker, sdist + wheel build. -[Unreleased]: https://github.com/CIR4FUN-EU/dpp-sdk-python/compare/v0.4.0...HEAD -[0.4.0]: https://github.com/CIR4FUN-EU/dpp-sdk-python/releases/tag/v0.4.0 -[0.1.1]: https://github.com/CIR4FUN-EU/dpp-sdk-python/compare/v0.1.0...v0.1.1 -[0.1.0]: https://github.com/CIR4FUN-EU/dpp-sdk-python/releases/tag/v0.1.0 +[Unreleased]: https://github.com/CIR4FUN-EU/python-dpp-sdk/compare/v0.5.0...main +[0.5.0]: https://github.com/CIR4FUN-EU/python-dpp-sdk/compare/v0.4.0...v0.5.0 +[0.4.0]: https://github.com/CIR4FUN-EU/python-dpp-sdk/compare/v0.2.1...v0.4.0 +[0.2.1]: https://github.com/CIR4FUN-EU/python-dpp-sdk/compare/v0.1.1...v0.2.1 +[0.1.1]: https://github.com/CIR4FUN-EU/python-dpp-sdk/compare/v0.1.0...v0.1.1 +[0.1.0]: https://github.com/CIR4FUN-EU/python-dpp-sdk/releases/tag/v0.1.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..275c5c0 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,130 @@ +# Contributing + +This page is for people working on the SDK itself. To use the SDK, start with the +[README](README.md). + +## Set up the development environment + +**Run from:** the repository root (the folder that contains `pyproject.toml`). + +```bash +git clone https://github.com/CIR4FUN-EU/python-dpp-sdk.git +cd python-dpp-sdk +python -m venv .venv +``` + +Activate the environment. Do this once in every new terminal: + +- Windows (PowerShell): `.venv\Scripts\Activate.ps1`. If PowerShell blocks it, run + `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` once. +- Linux/macOS: `source .venv/bin/activate`. If `python` is not found, use `python3`. + +Then install the SDK with its development tools and run the tests: + +```bash +python -m pip install -e ".[dev,release]" +python -m pytest +``` + +## Checks + +Run these before you open a pull request. CI runs the same commands on Python 3.11 and 3.12. + +```bash +python -m ruff check . +python -m ruff format --check . +python -m mypy +python -m pytest +``` + +Focused test sets, all without running services (the clients tests use `httpx.MockTransport`): + +| Area | Command | +| --- | --- | +| Core model and validation | `python -m pytest tests/test_core_model.py tests/test_core_json_codec.py tests/test_core_validation_parity.py` | +| DPP4Fun profile | `python -m pytest tests/test_dpp4fun_validation.py tests/test_transport_roundtrip.py` | +| HTTP clients | `python -m pytest tests/test_clients.py tests/test_end_to_end.py` | +| Documentation | `python -m pytest tests/test_documentation.py tests/test_readme_examples.py` | + +Live tests against running services are opt-in; see +[ADVANCED_OPERATIONS.md](examples/mock-services-demo/ADVANCED_OPERATIONS.md). + +## Documentation + +- `README.md` is also the PyPI project page. Use absolute `https://` links there; PyPI cannot + resolve relative links. +- The README and guides contain no version numbers; a test enforces this. +- The Python code blocks in `README.md`, `docs/model-guide.md`, and `docs/extending.md` are executed + by the documentation tests. Keep them runnable. +- `docs/dpp4fun-fields.md` is generated. After changing the DPP4Fun semantic profile, regenerate it: + + ```bash + python scripts/generate_dpp4fun_fields.py + ``` + + A test fails when the committed file is out of date. + +## Architecture diagrams + +Each maintained consumer diagram has an editable Mermaid source and a sibling SVG preview under +`docs/architecture/`. The render script uses the pinned Mermaid CLI image +`ghcr.io/mermaid-js/mermaid-cli/mermaid-cli:11.4.2` (digest +`sha256:99c983b3ab4e14033f2880bc1b9de17e5090b4515dabd63fe9cf8c0ae6130956`). Docker must be able to +pull that image before rendering. + +Run from the repository root: + +```powershell +powershell -ExecutionPolicy Bypass -File scripts/render_diagrams.ps1 +powershell -ExecutionPolicy Bypass -File scripts/render_diagrams.ps1 -VerifyOnly +``` + +```bash +pwsh -ExecutionPolicy Bypass -File scripts/render_diagrams.ps1 +pwsh -ExecutionPolicy Bypass -File scripts/render_diagrams.ps1 -VerifyOnly +``` + +`-VerifyOnly` checks that each source has an SVG and does not render or modify assets. Review SVG +diffs after rendering. The published diagram pairs are: + +| Diagram | Mermaid source | SVG preview | +|---|---|---| +| SDK overview | [python-sdk-overview.mmd](docs/architecture/python-sdk-overview.mmd) | [python-sdk-overview.svg](docs/architecture/python-sdk-overview.svg) | +| Client request flow | [python-client-request-flow.mmd](docs/architecture/python-client-request-flow.mmd) | [python-client-request-flow.svg](docs/architecture/python-client-request-flow.svg) | +| Core model | [python-core-model.mmd](docs/architecture/python-core-model.mmd) | [python-core-model.svg](docs/architecture/python-core-model.svg) | +| DPP4Fun model | [python-dpp4fun-model.mmd](docs/architecture/python-dpp4fun-model.mmd) | [python-dpp4fun-model.svg](docs/architecture/python-dpp4fun-model.svg) | + +## Mock-service verification + +Commands are in [ADVANCED_OPERATIONS.md](examples/mock-services-demo/ADVANCED_OPERATIONS.md). +This section records what the profiles and reports mean. + +| Profile (`examples/mock-services-demo/`) | Use | Status | +|---|---|---| +| `compose.yaml` defaults or `.env` | Local consumer profile with the current example Java images | Primary | +| `env/0.5.1.env` | Image-identity verification profile, also the CI default | Maintained | +| `env/pinned.env` | Earlier reproducible tag-plus-digest evidence | Historical | +| `env/0.5.0.env` | Earlier semantic-version evidence | Historical | +| `env/0.4.0.env` | Legacy compatibility evidence; demo commands need `--legacy` | Informational | + +Legacy runs are informational and do not satisfy the strict verification gate. Keep their reports +separate from the maintained profile's evidence. + +`demo` is the short educational journey. `python -m dpp_mock_services_demo.maintainer +sdk-contracts`, `live`, and `verify` produce the full SDK, service, package, image, and policy +evidence. `full`, `verify`, `integration`, `services`, and `all` remain compatibility commands. +Passing an educational step is evidence for that step only, not a compatibility certification. + +The manually triggered GitHub workflow `Mock services demo verification` builds both wheels, starts +a fresh disposable stack for the selected profile, runs installed-wheel verification, captures +evidence, and tears down its project. The PyPI release workflow does not start mock images; it runs +SDK-only installed-wheel verification. + +Strict reports use `PASSED`, `EXPECTED_ERROR`, `SKIPPED`, `FAILED`, and `NOT_IMPLEMENTED`. The +educational report uses `PASS`, `EXPECTED_ERROR`, `BLOCKED`, `SKIP`, and `FAIL`. Keep educational +success separate from the strict verification verdict. Clean up reports separately from Compose +projects, and keep the ones you need as evidence. + +## Releasing + +The release steps are in [RELEASING.md](RELEASING.md). diff --git a/README.md b/README.md index 316564d..3bb3e48 100644 --- a/README.md +++ b/README.md @@ -1,307 +1,280 @@ -# dpp-sdk (Python) +# dpp-sdk -`dpp-sdk` helps Python applications work with Digital Product Passports (DPPs). You can use it -to describe a passport, check it, turn it into JSON, and call a DPP repository or registry API. +`dpp-sdk` is a Python library for Digital Product Passports (DPPs). With it you can: -It is a client library, not a service. It does not run a repository or registry, store data, or -provide Docker, Spring, EDC, or dataspace features. The Mock-services demo is a separate optional -consumer of this SDK. +- describe a product passport in Python and check that it is valid, +- turn it into JSON and back, +- save it in a DPP **repository** and register it in a DPP **registry**. -## Architecture at a glance - -![Python SDK architecture](docs/architecture/python-sdk-overview.svg) - -`dpp_sdk.core` contains reusable DPP models and validation. `dpp_sdk.dpp4fun` builds its furniture -model, validation, and JSON mapping on Core, while `dpp_sdk.clients` stays model-independent and -performs the HTTP calls. The Mock-services demo is optional: it uses the installed SDK and public -clients against Java repository and registry images. - - -## Standards alignment - -**EN 18222:2026:** The SDK implements selected public DPP repository and registry API shapes -alongside the project DPP4Fun model, validation, and JSON rules. It is not a formal compliance implementation. It also makes no certification, legal-conformity, or production-readiness claim. - -Some API contracts were designed using confidential external technical specifications that cannot -be published here. This documentation describes only the behavior implemented by this project. - -### What the SDK supports - -- Create, read, update, read historical versions of, and delete repository passports. -- Register passport metadata with a registry. -- Read a compressed passport as an untyped JSON value when the service provides one. - -## Known standards limitations - -- The registry request has no backup-operator field or backup-operator operation. -- Compressed passports use a project-defined format. The SDK does not turn them into a typed - `Dpp4Fun` value automatically. -- Partial updates and element paths are only partly implemented. The SDK sends the documented - requests, but it does not provide a complete generic implementation of RFC 7396 JSON Merge Patch - or a complete implementation of RFC 9535 JSONPath. -- The Python SDK does not record lifecycle events or keep a lifecycle-event history. -- Authentication, backup/recovery, retries, persistence, and other production operations are not - provided. -- The SDK calls public `/v1/...` service routes. Mock demo `/internal/...` routes are not Python - client methods. -- No real EU registry integration, production security hardening, or production-operational - guarantee is provided. - - -## Start here - -Choose the smallest part that matches your job: - -| If you want to… | Start with… | -| --- | --- | -| describe reusable passport details | [Core module](src/dpp_sdk/core/README.md) | -| work with furniture passports | [DPP4Fun module](src/dpp_sdk/dpp4fun/README.md) | -| call a repository or registry API | [Clients module](src/dpp_sdk/clients/README.md) | -| see a complete consumer walkthrough | [SDK usage](docs/usage.md) | -| understand package boundaries | [SDK overview](docs/overview.md) | -| look up model fields and JSON/null semantics | [Model guide](docs/model-guide.md) | -| look up semantic validation rules | [Validation rules](docs/validation-rules.md) | -| try disposable mock services locally | [Mock-services demo](examples/mock-services-demo/README.md) | -| prepare a package release | [Release guide](RELEASING.md) | -| review changes or licensing | [Changelog](CHANGELOG.md) · [License](LICENSE) | - - -## Prerequisites - -- **All SDK users:** Python 3.11 or newer. -- **Only for the optional live Mock-services demo:** Docker Engine with Compose v2, Docker Buildx - for the demo's image-identity check, and access to the public GHCR image references. - -You do not need Docker, Compose, Buildx, or GHCR access to install the SDK, use its models, or run -the SDK-only demo checks. +The clients work with any repository or registry that implements the EN 18222 API. For local +testing, you can start example Java services with one command (see below). ## Install -**Purpose:** install the published SDK for an application. **Run from:** any directory; these -commands are directory-independent because pip downloads a named published distribution rather -than reading this checkout. **Prerequisites:** Python 3.11 or newer on `PATH` and access to the -selected package index. - -Choose the source deliberately: - -| Need | Install choice | Why | -| --- | --- | --- | -| ordinary application use | latest published `dpp-sdk` | uses the newest released distribution; no checkout is required | -| repeatable deployment or compatibility testing | an exact published version such as `dpp-sdk==0.4.0` | prevents a later release from changing the installed SDK | -| updates within one released minor line | a compatible version range such as `dpp-sdk~=0.4.0` | accepts compatible `0.4.x` fixes but not `0.5.0` | -| contribution, an unreleased fix, or reproducing checkout state | install this local checkout | uses the source currently in this repository; it is not a substitute for a released package in production | - -Release tags are named `v..` (for example, `v0.4.0`), while pip version -selectors omit the `v` (for example, `dpp-sdk==0.4.0`). +You need Python 3.11 or newer. -### PowerShell - -```powershell -python -m pip install dpp-sdk +```bash +pip install dpp-sdk ``` -### Linux/macOS +## Create, save, and register a passport -```bash -python -m pip install dpp-sdk -``` +### 1. Start a repository and a registry -### Install a selected published release +Skip this step if you already have EN 18222 services. Otherwise, start the example Java services +locally. This needs [Docker](https://docs.docker.com/get-started/get-docker/) (on Windows and macOS: +Docker Desktop, started). -Use an exact version when deployment must be reproducible. Use `~=` only when your application -has tested the compatible release line and can accept later patch releases. +Download the service definition into an empty folder and start it: -#### PowerShell +**Windows (PowerShell):** ```powershell -python -m pip install "dpp-sdk==0.4.0" -python -m pip install "dpp-sdk~=0.4.0" +curl.exe -O https://raw.githubusercontent.com/CIR4FUN-EU/python-dpp-sdk/main/examples/mock-services-demo/compose.yaml +docker compose up -d --wait ``` -#### Linux/macOS +**Linux/macOS:** ```bash -python -m pip install "dpp-sdk==0.4.0" -python -m pip install "dpp-sdk~=0.4.0" +curl -O https://raw.githubusercontent.com/CIR4FUN-EU/python-dpp-sdk/main/examples/mock-services-demo/compose.yaml +docker compose up -d --wait ``` -### Install this local checkout - -Use this only when you need the un-released source in this checkout, are contributing, or are -reproducing a source-state issue. Create the repository-local `.venv` once when it is absent; it is -local developer state and must not be committed. - -#### Create the development environment +When the command finishes, the repository runs at `http://localhost:8080` and the registry at +`http://localhost:8081`. -**Purpose:** create an isolated checkout environment with development and release tools. -**Run from:** the Python repository root, the directory containing `pyproject.toml`. -**Prerequisites:** Python 3.11 or newer on `PATH`. +### 2. Run the workflow -##### PowerShell +Save this as `workflow.py` in the same folder: -```powershell -python -m venv .venv -.\.venv\Scripts\python.exe -m pip install -e ".[dev,release]" +```python +import json +from datetime import UTC, datetime +from uuid import uuid4 + +from dpp_sdk import Dpp4FunJsonCodec, DppHeader, Granularity, validate_dpp4fun +from dpp_sdk.clients import ( + DppRegistryClient, + DppRepoClient, + DppRepresentation, + RegisterDppRequest, +) +from dpp_sdk.dpp4fun import ChairCharacteristics, Dpp4Fun, Nameplate + +# Replace these with your own services. Any EN 18222 repository/registry works. +REPO_URL = "http://localhost:8080" +REGISTRY_URL = "http://localhost:8081" + +# 1. Create a passport for a blue chair. +token = uuid4().hex # unique IDs, so you can run this script more than once +passport = Dpp4Fun.create( + DppHeader( + digital_product_passport_id=f"urn:example:dpp:{token}", + unique_product_identifier=f"urn:example:product:{token}", + granularity=Granularity.ITEM, + last_updated=datetime.now(UTC), + dpp_status="active", + economic_operator_id="urn:example:operator:demo", + ), + Nameplate.create(manufacturerName="Example Furniture"), + ChairCharacteristics.create(color="blue"), +) + +# 2. Check it before sending it anywhere. +report = validate_dpp4fun(passport) +if not report.is_valid: + raise SystemExit(f"Passport is not valid: {report.issues}") + +# 3. Save it in the repository. +full_json = json.loads(Dpp4FunJsonCodec().to_json(passport)) +with DppRepoClient(REPO_URL) as repository: + created = repository.create_dpp(full_json, DppRepresentation.FULL) +passport_id = created.digital_product_passport_id or passport.header.digital_product_passport_id +print("Saved passport:", passport_id) + +# 4. Register it, so others can find it. +with DppRegistryClient(REGISTRY_URL) as registry: + registration = registry.post_new_dpp_to_registry( + RegisterDppRequest( + unique_product_identifier=passport.header.unique_product_identifier, + digital_product_passport_id=passport_id, + unique_economic_operator_identifier=passport.header.economic_operator_id, + dpp_api_endpoint=f"{REPO_URL}/v1/dpps", + ) + ) +print("Registered:", registration.registration_id) + +# 5. Read it back as a typed passport. +with DppRepoClient(REPO_URL) as repository: + response = repository.read_dpp_by_id(passport_id, DppRepresentation.FULL) +loaded = Dpp4FunJsonCodec().from_json(json.dumps(response.payload)) +print("Manufacturer:", loaded.typed_aspect(Nameplate).field("manufacturerName").value) ``` -##### Linux/macOS +Run it: ```bash -python -m venv .venv -.venv/bin/python -m pip install -e ".[dev,release]" +python workflow.py ``` -**Expected result:** `.venv` contains an isolated editable SDK install plus the configured test, -lint, type-check, build, and package-check tools. **Cleanup:** remove `.venv` only when you no -longer need this checkout environment. +You should see three lines similar to: -#### PowerShell - -```powershell -.\.venv\Scripts\python.exe -m pip install . +```text +Saved passport: urn:example:dpp:a06675bc10d4439e9b6de17e899c51b1 +Registered: e532b61a-d1cb-47dd-8589-ce6a2f387de8 +Manufacturer: Example Furniture ``` -#### Linux/macOS +The clients send and return plain JSON (Python dicts), and the codecs read and write JSON strings. +That is why the script uses `json.loads()` before sending and `json.dumps()` after reading. -```bash -.venv/bin/python -m pip install . -``` +Steps 1 and 2 of the script (create and check) work without any service. -`dpp-sdk` is the single package that contains the core, DPP4Fun, clients, and public payload -models. The separately installed `dpp-sdk-mock-services-demo` package is reference/demo code, not -a dependency that ordinary SDK users need. +To also read, update, look up older versions of, and delete a passport, see the +[full lifecycle example](https://github.com/CIR4FUN-EU/python-dpp-sdk/blob/main/examples/consumer_lifecycle.py). -**Expected result:** the same interpreter imports the intended SDK: +### 3. Stop the services -```powershell -.\.venv\Scripts\python.exe -c "import dpp_sdk; print(dpp_sdk.__version__)" -``` +Run this in the same folder: ```bash -.venv/bin/python -c "import dpp_sdk; print(dpp_sdk.__version__)" +docker compose down ``` -**Cleanup:** none. +The stored data is kept for next time. To delete it too, run `docker compose down -v`. -## First use from this checkout - -All local commands in this section run from the repository root, the directory containing -`pyproject.toml`. - -1. Choose a published install above, or create `.venv` and install this checkout. -2. Run the import probe above, then use the small SDK example below. -3. Run the SDK-only educational walkthrough after following the isolated-wheel setup in the - [Mock-services demo guide](examples/mock-services-demo/README.md#quick-start): - `./.mock-services-demo-venv/Scripts/python.exe -m dpp_mock_services_demo sdk` on PowerShell, or - `./.mock-services-demo-venv/bin/python -m dpp_mock_services_demo sdk` on Linux/macOS. Use - `--summary` for compact verification or `--json` for machine-readable evidence. -4. Run SDK tests with the development interpreter; focused core, validation, client, and - documentation commands are owned by the [release guide](RELEASING.md). -5. Only when you need live interoperability, follow the [Mock-services demo guide] - (examples/mock-services-demo/README.md) to start a uniquely named disposable project, run - `demo`, `full`, or `verify`, inspect its report/logs, and tear down only that project. - -The demo guide owns its longer service commands and troubleshooting. Docker is never required for -the installation, SDK example, or SDK-only demo. - -## A small useful example - -Parse a furniture passport, check it, and turn it back into JSON: - -```python -from dpp_sdk import from_json, to_json, validate_dpp4fun +## How the SDK is organized +Diagram: your application uses the DPP4Fun profile, the Core models, and the Clients. DPP4Fun builds on Core. The Clients send HTTP requests to repository and registry APIs. -def check_passport(raw_json: str) -> str: - passport = from_json(raw_json) - validate_dpp4fun(passport) - return to_json(passport) -``` +| Part | What it does | +| --- | --- | +| `dpp_sdk.core` | General passport model, validation, and JSON conversion. | +| `dpp_sdk.dpp4fun` | Ready-made furniture passport (chairs, sofas, …) built on Core. | +| `dpp_sdk.clients` | HTTP clients for repositories and registries. They send and receive plain JSON and don't depend on the models. | -`from_json()` checks that the JSON has the right shape. `validate_dpp4fun()` checks the extra -business rules. Run validation before relying on a passport or sending it to a service. +## The DPP4Fun furniture passport -## Store and read a passport +`dpp_sdk.dpp4fun` is a ready-to-use passport for furniture, based on the DPP4Fun profile (D4.1). +You fill it with typed Python objects instead of building raw DataElements yourself. -Once you have a valid `Dpp4Fun` value and a repository URL, the generic client can store and read -it. The same pattern works with your own model when you provide a codec and validator. - -```python -from dpp_sdk import Dpp4Fun, Dpp4FunJsonCodec, validate_dpp4fun -from dpp_sdk.clients import DppRepoClient - - -def store_and_read(dpp: Dpp4Fun, repository_url: str) -> Dpp4Fun: - with DppRepoClient( - repository_url, - codec=Dpp4FunJsonCodec(), - validator=validate_dpp4fun, - ) as repository: - created = repository.create_dpp(dpp) - return repository.read_dpp_by_id(created.dppId) -``` - -For all operations, request and response payloads, errors, partial updates, and resource ownership, -see the [clients module guide](src/dpp_sdk/clients/README.md). +A passport has a header plus any of these 19 **aspects**: +| Topic | Aspects | +| --- | --- | +| Identity and origin | `Nameplate`, `Traceability`, `ProductRelationship` | +| Environment and health | `EnvironmentalImpact`, `CarbonFootprint`, `BillOfMaterials`, `HealthImpact` | +| Circularity | `Durability`, `Reliability`, `Repairability`, `Refurbishment`, `Remanufacturing`, `Reusability`, `Recyclability`, `Upgradability`, `WasteManagement` | +| Use and rights | `Usage`, `Safety`, `RightOfEndUsers` | -### Technical details +It can also have one **product type** with its own fields: `ChairCharacteristics`, +`CribCharacteristics`, `FabricCharacteristics`, `LockerCharacteristics`, +`ParticleBoardCharacteristics`, or `SofaCharacteristics`. -Repository creation uses `POST /v1/dpps`; registry registration uses `POST /v1/registerDPP`. A -registration request identifies its repository with `dppApiEndpoint="https://repo.example.com"`. -Full reads use a typed model; compressed and fine-grained reads return the JSON value supplied by -the service. The typed compressed-read method is `read_compressed_dpp_by_id()`. +Reusable **building blocks** go inside the aspects, for example `Material`, `Component`, `Part`, +`Certificate`, `Instruction`, `Contact`, and records such as `ManufacturingRecord` or `RepairRecord`. -`UpdateDataElementRequest` remains importable for compatibility, while the normal fine-grained -update sends its value directly. The client guide explains exact JSON field names, error types, and -supported paths. +What you can do with it: -`read_dpp_version_by_product_id_and_date()` remains a legacy compatibility only route. New code -should use `read_dpp_version_by_id_and_date()` with a DPP ID. +- **Create and edit** passports with typed fields. Edits return a new copy (`with_field()`). +- **Use units safely:** `Dpp4FunQuantity` pairs a number with a `LengthUnit`, `MassUnit`, or + `VolumeUnit`. Values with the wrong kind of unit are rejected. +- **Validate** against the D4.1 rules with `validate_dpp4fun()`. You get a list of issues, each + with the path of the field it concerns. +- **Convert** to and from FULL and COMPRESSED JSON with `Dpp4FunJsonCodec`. The built-in profile + describes all 488 fields, so COMPRESSED JSON can be expanded again. +- **Keep extra data:** fields that are not part of the profile are kept, not dropped. -Semantic validation is fail-fast. Models use immutable tuples for contracted collections; use -`with_updates()` to make a checked replacement instead of changing a model in place. Mapping -failures in client code use `DppMappingClientError`; an HTTPX client you provide stays caller-owned. +Field names are the camelCase IDs used in JSON (`manufacturerName=`, not `manufacturer_name=`). +The [field reference](https://github.com/CIR4FUN-EU/python-dpp-sdk/blob/main/docs/dpp4fun-fields.md) +lists all of them per aspect, and the [model guide](https://github.com/CIR4FUN-EU/python-dpp-sdk/blob/main/docs/model-guide.md) +shows how to build, read, edit, and validate passports. -## Optional Mock-services demo +## Handle errors -Use Docker Compose to start the disposable repository and registry services, run the connected demo, -then stop and remove the project. **Run from:** the Python repository root. +Validation never raises; it returns a report (`report.is_valid`, `report.issues`). Everything else +raises a specific exception: -```powershell -$demoDir = (Resolve-Path .\examples\mock-services-demo).Path -$envFile = Join-Path $demoDir ".env" -if (-not (Test-Path -LiteralPath $envFile)) { - Copy-Item (Join-Path $demoDir ".env.example") $envFile -} -$composeFile = Join-Path $demoDir "compose.yaml" -$project = "dpp-mock-services-demo-local" -docker compose --env-file $envFile -f $composeFile -p $project up -d --wait -& .\.venv\Scripts\python.exe -m dpp_mock_services_demo demo --env-file $envFile -docker compose --env-file $envFile -f $composeFile -p $project down -v -``` +| Exception | Raised when | +| --- | --- | +| `DppNetworkClientError` | The service cannot be reached, or the request times out. | +| `DppApiClientError` | The service answers with an API error, for example `ClientErrorResourceNotFound`. Has the HTTP status and the service messages. | +| `DppHttpClientError` | The service answers with an HTTP error that is not an API error envelope. | +| `DppMappingClientError` | A request cannot be serialized, or a response is not a valid envelope. | +| `DppMappingError` | Passport JSON cannot be read or written (bad JSON, wrong structure, missing header fields). | +| `DppValidationError` | Only from `from_json_and_validate()`: the JSON was read, but the passport is not valid. | -## Linux/macOS +The client exceptions are in `dpp_sdk.clients`; the model exceptions are in `dpp_sdk`. -```bash -demo_dir="$(cd ./examples/mock-services-demo && pwd)" -env_file="$demo_dir/.env" -[ -f "$env_file" ] || cp "$demo_dir/.env.example" "$env_file" -docker compose --env-file "$env_file" -f "$demo_dir/compose.yaml" -p dpp-mock-services-demo-local up -d --wait -.venv/bin/python -m dpp_mock_services_demo demo --env-file "$env_file" -docker compose --env-file "$env_file" -f "$demo_dir/compose.yaml" -p dpp-mock-services-demo-local down -v +```python +from dpp_sdk.clients import DppApiClientError, DppNetworkClientError + +try: + with DppRepoClient(REPO_URL) as repository: + repository.read_dpp_by_id(passport_id) +except DppApiClientError as error: + print("Service returned an error:", error) +except DppNetworkClientError: + print("Service not reachable") ``` -For tests, alternate configuration, logs, and troubleshooting, see the -[Mock-services demo guide](examples/mock-services-demo/README.md) and -[ADVANCED_OPERATIONS.md](examples/mock-services-demo/ADVANCED_OPERATIONS.md). - -## Documentation and next steps +## Documentation -For a first integration, follow the [SDK usage guide](docs/usage.md). Use the [SDK overview] -(docs/overview.md) to understand package boundaries, the [model guide](docs/model-guide.md) for -field/default/null behavior, the [validation guide](docs/validation-guide.md) and -[validation-rule reference](docs/validation-rules.md) for validation and codec behavior, and the -[clients guide](src/dpp_sdk/clients/README.md) for every public HTTP operation and payload. The -[release guide](RELEASING.md) owns development and release validation; the separate -[Mock-services demo](examples/mock-services-demo/README.md) owns its optional service lifecycle. +| If you want to… | Read | +| --- | --- | +| build, read, edit, validate, and convert passports | [Model guide](https://github.com/CIR4FUN-EU/python-dpp-sdk/blob/main/docs/model-guide.md) | +| find the field names of a DPP4Fun aspect | [DPP4Fun field reference](https://github.com/CIR4FUN-EU/python-dpp-sdk/blob/main/docs/dpp4fun-fields.md) | +| see every client method and error | [HTTP clients guide](https://github.com/CIR4FUN-EU/python-dpp-sdk/blob/main/docs/clients-guide.md) | +| build passports for another product group | [Building your own profile](https://github.com/CIR4FUN-EU/python-dpp-sdk/blob/main/docs/extending.md) | +| trace behavior to EN 18222/18223/18219 or D4.1 | [Standards alignment and validation rules](https://github.com/CIR4FUN-EU/python-dpp-sdk/blob/main/docs/conformance.md) | +| run the guided demo with test scenarios | [Mock-services demo](https://github.com/CIR4FUN-EU/python-dpp-sdk/blob/main/examples/mock-services-demo/README.md) | +| see what changed, or upgrade from an older version | [Changelog](https://github.com/CIR4FUN-EU/python-dpp-sdk/blob/main/CHANGELOG.md) | +| contribute, run the tests, or work on this repository | [Contributing guide](https://github.com/CIR4FUN-EU/python-dpp-sdk/blob/main/CONTRIBUTING.md) | + +## Terms used here + +- **DPP (Digital Product Passport):** a structured record about one product, model, or batch. +- **Repository:** a service that stores passports. +- **Registry:** a service that records where a passport is stored, so it can be found. +- **FULL / COMPRESSED:** two JSON forms of a passport. FULL includes type information for every + field; COMPRESSED is shorter and contains only the values. +- **DataElement:** one field or group of fields inside a passport. +- **DPP4Fun:** a furniture passport profile, defined in the project document D4.1. +- **AAS (Asset Administration Shell):** the template format in which the DPP4Fun fields are published. + +## Standards and limitations + +The SDK follows selected parts of these standards. This is not a claim of full conformance, +certification, or legal compliance. + +- **EN 18222:2026** (repository and registry API): Clauses 4.2–4.8, 5.2, 6.2–6.3, 7.2, 8.1–8.4. +- **EN 18223:2026** (passport model and JSON): Clauses 4.1.2.1–4.1.2.9 and 5.2.2–5.2.9. +- **EN 18219:2026** (identifiers): local syntax checks for GS1 Digital Link, GS1 GLN, and + Identification Link only. The SDK does not issue, resolve, or prove uniqueness of identifiers. +- **DPP4Fun D4.1 v1.0**, Sections 3.2.1–3.2.2. Where D4.1 contradicts itself, the + [standards page](https://github.com/CIR4FUN-EU/python-dpp-sdk/blob/main/docs/conformance.md#source-alignment-and-source-conflicts) + explains which choice the SDK makes. + +Not included: + +- No authentication, retries, caching, or automatic pagination (you pass `cursor` yourself). +- No complete JSON Merge Patch (RFC 7396) or JSONPath (RFC 9535) implementation. The client sends + your update as-is, and the service decides how to apply it. +- No registry delete or read-back, no backup-operator support, and no lifecycle-event history. +- COMPRESSED JSON is the JSON API shape used by the Java services, not the XML form in + EN 18223 Annex B. +- No connection to a real EU registry, and no production hardening. + +## Versioning + +`dpp-sdk` is in the 0.x phase: a new minor version may contain breaking changes. Every +breaking change is listed in the [changelog](https://github.com/CIR4FUN-EU/python-dpp-sdk/blob/main/CHANGELOG.md) with migration notes. The public API +is what `dpp_sdk`, `dpp_sdk.core`, `dpp_sdk.dpp4fun`, and `dpp_sdk.clients` export; modules and +names starting with `_` are internal. + +## Contributing and license + +To work on the SDK itself, see the [contributing guide](https://github.com/CIR4FUN-EU/python-dpp-sdk/blob/main/CONTRIBUTING.md). The release steps are +in the [release guide](https://github.com/CIR4FUN-EU/python-dpp-sdk/blob/main/RELEASING.md). The license is in +[LICENSE](https://github.com/CIR4FUN-EU/python-dpp-sdk/blob/main/LICENSE). diff --git a/RELEASING.md b/RELEASING.md index 72d43d9..1e5ac1b 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -3,24 +3,43 @@ This repository publishes the `dpp-sdk` Python package from version tags matching `v..`. +## Version bump checklist + +The SDK version appears in these places only. Update all of them together: + +- `src/dpp_sdk/__init__.py` (`__version__`, the single source for the package build) +- `CHANGELOG.md` (move `[Unreleased]` entries under the new version, add the release date, and + add a compare link for the new version at the bottom) +- `examples/mock-services-demo/pyproject.toml` (the `dpp-sdk==` pin) +- `.github/workflows/mock-services-demo.yml` (the `dpp_sdk--py3-none-any.whl` file name) +- `tests/test_release_validation.py` (the expected package version) + +The example Java service version appears only in `examples/mock-services-demo/compose.yaml` and +`examples/mock-services-demo/.env.example` (the image tags). + +The README and guides deliberately contain no version numbers, and a test enforces this. + ## Before tagging **Run from:** the Python repository root, the directory containing `pyproject.toml`. -**Prerequisites:** the checkout development environment. - -1. Confirm `src/dpp_sdk/__init__.py`, `CHANGELOG.md`, and the intended tag use the same version. -2. Run the configured validation: - - ```powershell - .\.venv\Scripts\python.exe -m ruff check . - .\.venv\Scripts\python.exe -m ruff format --check . - .\.venv\Scripts\python.exe -m mypy - .\.venv\Scripts\python.exe -m pytest - .\.venv\Scripts\python.exe -m build - .\.venv\Scripts\python.exe -m twine check dist/* +**Prerequisites:** the development environment from [CONTRIBUTING.md](CONTRIBUTING.md), activated. + +1. Work through the version bump checklist above. +2. Run the configured validation. The commands are the same in PowerShell and Bash: + + ```bash + python -c "from dpp_sdk import __version__; print(__version__)" + python -m ruff check . + python -m ruff format --check . + python -m mypy + python -m pytest ``` -3. Inspect the wheel and verify a fresh installation without source-tree imports. + The first command prints the version you are about to release. + +3. Build, check, and inspect the packages and verify a fresh installation without source-tree + imports. Use the [disposable build](#disposable-build-and-archive-inspection) below; it never + touches `dist/`. 4. Confirm the repository is clean and review the release diff. ## Disposable build and archive inspection @@ -31,13 +50,15 @@ These commands run from the Python repository root and work when that path conta PowerShell: ```powershell -$buildRoot = Join-Path ([System.IO.Path]::GetTempPath()) "dpp-sdk-release-$PID" -$testEnv = Join-Path ([System.IO.Path]::GetTempPath()) "dpp-sdk-wheel-$PID" -New-Item -ItemType Directory -Path $buildRoot | Out-Null -.\.venv\Scripts\python.exe -m build --outdir $buildRoot -.\.venv\Scripts\python.exe -m twine check "$buildRoot\*" +$runId = [guid]::NewGuid().ToString("N") +$buildRoot = Join-Path ([System.IO.Path]::GetTempPath()) "dpp-sdk-release-$runId" +$testEnv = Join-Path ([System.IO.Path]::GetTempPath()) "dpp-sdk-wheel-$runId" +if ((Test-Path -LiteralPath $buildRoot) -or (Test-Path -LiteralPath $testEnv)) { throw "Temporary path already exists; choose a new run ID." } +New-Item -ItemType Directory -Path $buildRoot, $testEnv | Out-Null +python -m build --outdir $buildRoot +python -m twine check "$buildRoot\*" Get-ChildItem $buildRoot -.\.venv\Scripts\python.exe -m zipfile -l (Get-ChildItem "$buildRoot\*.whl").FullName +python -m zipfile -l (Get-ChildItem "$buildRoot\*.whl").FullName python -m venv $testEnv & "$testEnv\Scripts\python.exe" -m pip install (Get-ChildItem "$buildRoot\*.whl").FullName Push-Location ([System.IO.Path]::GetTempPath()) @@ -50,19 +71,20 @@ Linux/macOS: ```bash build_root="$(mktemp -d)" test_env="$(mktemp -d)" -.venv/bin/python -m build --outdir "$build_root" -.venv/bin/python -m twine check "$build_root"/* +import_root="$(mktemp -d)" +python -m build --outdir "$build_root" +python -m twine check "$build_root"/* ls -la "$build_root" -.venv/bin/python -m zipfile -l "$(find "$build_root" -maxdepth 1 -name '*.whl' -print -quit)" +python -m zipfile -l "$(find "$build_root" -maxdepth 1 -name '*.whl' -print -quit)" python -m venv "$test_env" "$test_env/bin/python" -m pip install "$(find "$build_root" -maxdepth 1 -name '*.whl' -print -quit)" -(cd "$(mktemp -d)" && "$test_env/bin/python" -I -c 'import dpp_sdk; print(dpp_sdk.__version__)') -rm -rf "$test_env" "$build_root" +(cd "$import_root" && "$test_env/bin/python" -I -c 'import dpp_sdk; print(dpp_sdk.__version__)') +rm -rf "$test_env" "$build_root" "$import_root" ``` Expected result: build and Twine succeed, the wheel lists only intended package files, and the fresh interpreter imports `dpp_sdk` outside the checkout. If build tooling is missing, recreate the -development environment from the README. Keep the temporary directories when a command fails so +development environment from [CONTRIBUTING.md](CONTRIBUTING.md). Keep the temporary directories when a command fails so their artifacts can be inspected; remove only directories created by this sequence after review. ## Publishing @@ -72,18 +94,19 @@ sdist and wheel, checks metadata and README rendering, publishes first to TestPy then uses the protected `pypi` environment for production publication. Run these from the repository root, in order, after the release changes are merged into -the default branch on GitHub: +the default branch on GitHub. Replace `X.Y.Z` with the version from `src/dpp_sdk/__init__.py`. +The git commands are the same in PowerShell and Bash: ```bash # 1. Get the merged release commit onto the default branch locally. git checkout main -git pull +git pull https://github.com/CIR4FUN-EU/python-dpp-sdk.git main -# 2. Tag that commit with the version (must match src/dpp_sdk/__init__.py and CHANGELOG.md). -git tag v0.4.0 +# 2. Tag that commit with the version. +git tag vX.Y.Z # 3. Push the tag to GitHub to start the release workflow. -git push origin v0.4.0 +git push https://github.com/CIR4FUN-EU/python-dpp-sdk.git vX.Y.Z ``` The tag must point at a commit already on GitHub's default branch, and its name must match diff --git a/docs/architecture/python-client-request-flow.mmd b/docs/architecture/python-client-request-flow.mmd index 8e4eccf..6223bc3 100644 --- a/docs/architecture/python-client-request-flow.mmd +++ b/docs/architecture/python-client-request-flow.mmd @@ -1,5 +1,7 @@ flowchart LR - App["Python application"] -->|uses| Repo["Repository Client
DppRepoClient"] - App -->|uses| Registry["Registry Client
DppRegistryClient"] - Repo -->|HTTP| RepoApi["Public repository API"] - Registry -->|HTTP| RegistryApi["Public registry API"] + App["Python application
JSON values"] -->|uses| Repo["DppRepoClient"] + App -->|uses| Registry["DppRegistryClient"] + Repo -->|generic JSON| Http["Shared HTTPX transport
response envelopes"] + Registry -->|generic JSON| Http + Http -->|repository requests| RepoApi["External repository API"] + Http -->|registry requests| RegistryApi["External registry API"] diff --git a/docs/architecture/python-client-request-flow.svg b/docs/architecture/python-client-request-flow.svg index 25adcc7..e1384f8 100644 --- a/docs/architecture/python-client-request-flow.svg +++ b/docs/architecture/python-client-request-flow.svg @@ -1 +1 @@ -

uses

uses

HTTP

HTTP

Python application

Repository Client
DppRepoClient

Registry Client
DppRegistryClient

Public repository API

Public registry API

\ No newline at end of file +

uses

uses

generic JSON

generic JSON

repository requests

registry requests

Python application
JSON values

DppRepoClient

DppRegistryClient

Shared HTTPX transport
response envelopes

External repository API

External registry API

\ No newline at end of file diff --git a/docs/architecture/python-core-model.mmd b/docs/architecture/python-core-model.mmd index 251f83b..0bc3946 100644 --- a/docs/architecture/python-core-model.mmd +++ b/docs/architecture/python-core-model.mmd @@ -1,5 +1,8 @@ -flowchart LR - App["Python application"] -->|uses| Core["Reusable Core
dpp_sdk.core"] - Core -->|provides| Models["Frozen DPP models"] - Core -->|provides| Validation["Explicit core validation"] - Core -->|provides| Errors["Shared DPP errors"] +flowchart TB + Passport["DigitalProductPassport"] -->|contains one| Header["DppHeader"] + Passport -->|contains ordered| Elements["DataElements"] + Elements --> Collection["Collection
nested elements"] + Elements --> Single["Single value"] + Elements --> Multi["Multiple values
nested elements"] + Elements --> Language["Multilanguage values"] + Elements --> Resource["Related resource"] diff --git a/docs/architecture/python-core-model.svg b/docs/architecture/python-core-model.svg index 6c95370..1f3d338 100644 --- a/docs/architecture/python-core-model.svg +++ b/docs/architecture/python-core-model.svg @@ -1 +1 @@ -

uses

provides

provides

provides

Python application

Reusable Core
dpp_sdk.core

Frozen DPP models

Explicit core validation

Shared DPP errors

\ No newline at end of file +

contains one

contains ordered

DigitalProductPassport

DppHeader

DataElements

Collection
nested elements

Single value

Multiple values
nested elements

Multilanguage values

Related resource

\ No newline at end of file diff --git a/docs/architecture/python-dpp4fun-model.mmd b/docs/architecture/python-dpp4fun-model.mmd index 6e59fd4..a9acf0a 100644 --- a/docs/architecture/python-dpp4fun-model.mmd +++ b/docs/architecture/python-dpp4fun-model.mmd @@ -1,5 +1,8 @@ -flowchart LR - Dpp4Fun["DPP4Fun
dpp_sdk.dpp4fun"] -->|depends on| Core["Reusable Core
dpp_sdk.core"] - Dpp4Fun -->|provides| Model["Furniture aggregate"] - Dpp4Fun -->|provides| Validation["Explicit furniture validation"] - Dpp4Fun -->|provides| Transport["Flat / nested JSON mapping"] +flowchart TB + Facade["Dpp4Fun
immutable facade"] -->|holds one| Core["Core DigitalProductPassport"] + Aspects["Typed aspects
19 general + 6 Characteristics"] -->|read / edit| Facade + Aspects -->|map to| Elements["Core DataElements"] + Profile["Packaged semantic profile"] -->|defines fields| Aspects + Profile -->|supplies metadata| Codec["DPP4Fun JSON codec"] + Facade -->|checked by| Validation["Profile validation"] + Codec -->|uses| CoreCodec["Core FULL / COMPRESSED codec"] diff --git a/docs/architecture/python-dpp4fun-model.svg b/docs/architecture/python-dpp4fun-model.svg index 4355759..730a32f 100644 --- a/docs/architecture/python-dpp4fun-model.svg +++ b/docs/architecture/python-dpp4fun-model.svg @@ -1 +1 @@ -

depends on

provides

provides

provides

DPP4Fun
dpp_sdk.dpp4fun

Reusable Core
dpp_sdk.core

Furniture aggregate

Explicit furniture validation

Flat / nested JSON mapping

\ No newline at end of file +

holds one

read / edit

map to

defines fields

supplies metadata

checked by

uses

Dpp4Fun
immutable facade

Core DigitalProductPassport

Typed aspects
19 general + 6 Characteristics

Core DataElements

Packaged semantic profile

DPP4Fun JSON codec

Profile validation

Core FULL / COMPRESSED codec

\ No newline at end of file diff --git a/docs/architecture/python-sdk-overview.mmd b/docs/architecture/python-sdk-overview.mmd index 5d8fcc9..87d3751 100644 --- a/docs/architecture/python-sdk-overview.mmd +++ b/docs/architecture/python-sdk-overview.mmd @@ -1,24 +1,12 @@ -flowchart TB - subgraph Normal["Normal application use"] - direction LR - App["My Python application"] - Core["Reusable Core
dpp_sdk.core"] - Dpp4Fun["DPP4Fun
dpp_sdk.dpp4fun"] - Clients["HTTP Clients
dpp_sdk.clients"] - APIs["DPP repository / registry APIs"] +flowchart LR + App["Python application"] -->|uses| Dpp4Fun + App -->|uses| Core + App -->|uses| Clients + Demo["Optional demo"] -->|uses installed SDK| Clients - App -->|uses directly| Core - App -->|uses| Dpp4Fun - Dpp4Fun -->|depends on| Core - App -->|uses| Clients - Clients -->|HTTP| APIs + subgraph SDK["Python SDK"] + Dpp4Fun["DPP4Fun
furniture profile"] -->|depends on| Core["Core
models + validation + JSON"] + Clients["Clients
generic JSON + HTTPX"] end - subgraph Optional["Optional demo / verification"] - direction LR - Demo["Mock-services demo
uses the installed Python SDK"] - Java["Java mock-service APIs
repository + registry images"] - Demo -->|HTTP through public Clients| Java - end - - Normal ~~~ Optional + Clients -->|HTTP| APIs["External repository / registry APIs
Java demo images when used"] diff --git a/docs/architecture/python-sdk-overview.svg b/docs/architecture/python-sdk-overview.svg index 278c030..613255c 100644 --- a/docs/architecture/python-sdk-overview.svg +++ b/docs/architecture/python-sdk-overview.svg @@ -1 +1 @@ -

Optional demo / verification

HTTP through public Clients

Mock-services demo
uses the installed Python SDK

Java mock-service APIs
repository + registry images

Normal application use

uses directly

uses

depends on

uses

HTTP

My Python application

Reusable Core
dpp_sdk.core

DPP4Fun
dpp_sdk.dpp4fun

HTTP Clients
dpp_sdk.clients

DPP repository / registry APIs

\ No newline at end of file +

Python SDK

uses

uses

uses

uses installed SDK

depends on

HTTP

Python application

DPP4Fun
furniture profile

Core
models + validation + JSON

Clients
generic JSON + HTTPX

Optional demo

External repository / registry APIs
Java demo images when used

\ No newline at end of file diff --git a/docs/clients-guide.md b/docs/clients-guide.md new file mode 100644 index 0000000..80bc7fb --- /dev/null +++ b/docs/clients-guide.md @@ -0,0 +1,173 @@ +# HTTP clients guide + +![HTTP client request flow](architecture/python-client-request-flow.svg) + +The repository and registry clients send and receive plain JSON. They don't build, check, or +convert passport models; your application does that (for example with `Dpp4FunJsonCodec`). +Create a client with the service's base URL. Optionally pass your own `httpx.Client` with +`client=`. + +```python +from dpp_sdk.clients import DppRepoClient, DppRepresentation + +with DppRepoClient("https://repo.example.com") as repo: + created = repo.create_dpp( + { + "digitalProductPassportId": "urn:example:dpp:1", + "uniqueProductIdentifier": "urn:example:product:1", + "granularity": "model", + "dppSchemaVersion": "1.0", + "dppStatus": "active", + "lastUpdated": "2026-10-16T00:00:00Z", + "economicOperatorId": "urn:example:operator:1", + "contentSpecificationIds": [], + "elements": [ + { + "elementId": "nameplate", + "objectType": "DataElementCollection", + "elements": [ + { + "elementId": "manufacturerName", + "objectType": "SingleValuedDataElement", + "value": "Example Furniture", + "valueDataType": "xsd:string", + } + ], + } + ], + }, + DppRepresentation.FULL, + ) + if created.digital_product_passport_id is None: + raise RuntimeError("repository did not return a passport ID") + try: + result = repo.read_dpp_by_id(created.digital_product_passport_id, DppRepresentation.FULL) + passport_json = result.payload + finally: + repo.delete_dpp_by_id(created.digital_product_passport_id) +``` + +Both clients are synchronous. Use them in a `with` block or call `close()`. This closes only a +client the SDK created itself; an `httpx.Client` you passed with `client=` remains caller owned, +so close it yourself. Default timeouts: 15 seconds per request, 5 seconds to connect. The base URL +must be an absolute `http://` or `https://` URL with a host and no query or fragment. +Requests send `Accept: application/json`, and `Content-Type: application/json` when they have a +body. + +## Public operations + +Repository reads and updates return `DppApiResponse`. Create returns `CreateDppResponse`, delete +returns `DeleteDppResponse`, and batch identifier lookup returns `ReadDppIdsResponse`. Registry +registration returns `RegisterDppResponse`. These wrappers expose Python `snake_case` attributes +while retaining the canonical JSON aliases. + +| Public method | HTTP operation and payload | Result | +| --- | --- | --- | +| `create_dpp(dpp, representation=COMPRESSED)` | `POST /v1/dpps`; JSON-compatible DPP value | `CreateDppResponse` | +| `read_dpp_by_id(dpp_id, representation=COMPRESSED)` | `GET /v1/dpps/{dpp_id}` | `DppApiResponse` | +| `read_dpp_by_product_id(product_id, representation=COMPRESSED)` | `GET /v1/dppsByProductId/{product_id}` | `DppApiResponse` | +| `read_dpp_version_by_id_and_date(dpp_id, date, representation=COMPRESSED)` | `GET /v1/dppsByIdAndDate/{dpp_id}?date=...`; `date` must be timezone-aware | `DppApiResponse` | +| `read_dpp_ids_by_product_ids(product_ids, limit=None, cursor=None)` | `POST /v1/dppsByProductIds`; `ReadDppIdsRequest` JSON | `ReadDppIdsResponse` | +| `update_dpp_by_id(dpp_id, partial_dpp, representation=COMPRESSED)` | `PATCH /v1/dpps/{dpp_id}`; forwards the supplied JSON object | `DppApiResponse` | +| `delete_dpp_by_id(dpp_id)` | `DELETE /v1/dpps/{dpp_id}` | `DeleteDppResponse` | +| `read_data_element(dpp_id, element_id_path, representation=COMPRESSED)` | `GET /v1/dpps/{dpp_id}/elements/{element_id_path}` | `DppApiResponse` | +| `update_data_element(dpp_id, element_id_path, data_element, representation=COMPRESSED)` | `PATCH /v1/dpps/{dpp_id}/elements/{element_id_path}`; JSON DataElement value | `DppApiResponse` | +| `post_new_dpp_to_registry(request)` | `POST /v1/registerDPP`; `RegisterDppRequest` or JSON-compatible mapping | `RegisterDppResponse` | + +`COMPRESSED` is the default. Pass `DppRepresentation.FULL` when you want to decode the result into +a typed passport; see [Representation and wire behavior](#representation-and-wire-behavior). + +## Envelopes and errors + +Every response has the JSON keys `statusCode`, `payload`, and `message`. In Python: + +- `status_code`: a `DppStatusCode`, or `None`. +- `payload`: any JSON value. It may be missing where the API allows it. Create and registration + identifiers are optional. Batch lookup requires `dppIdentifiers`, and element calls require a + non-null payload. +- `message`: an optional list of `DppApiMessage`. Each one has `message_type` and `text` + (required, blank text allowed) and optional `code`, `correlation_id`, and `timestamp`. + +| HTTP response | Client behavior | +| --- | --- | +| 2xx with `Success`, `SuccessCreated`, or `SuccessAccepted` | Return the mapped operation response. | +| Repository 204 | Return `SuccessNoContent` without a payload. | +| 2xx with an API error `statusCode` | Raise `DppApiClientError`. | +| Non-2xx with a usable API error envelope | Raise `DppApiClientError` with HTTP status and messages. | +| Non-2xx without a usable API error envelope | Raise `DppHttpClientError` with HTTP status and raw body. | + +| Exception | Meaning | +| --- | --- | +| `DppNetworkClientError` | HTTPX connection, transport, or timeout failure | +| `DppHttpClientError` | Non-2xx response without a usable error envelope | +| `DppApiClientError` | Non-success API status, on either a 2xx or structured non-2xx response | +| `DppMappingClientError` | JSON serialization, malformed envelope, or missing required operation payload | + +Unknown fields in a response are ignored. Serialization errors are raised before the request is +sent. + +`DppStatusCode` also recognizes `ClientErrorBadRequest`, `ClientNotAuthorized`, `ClientForbidden`, +`ClientMethodNotAllowed`, `ClientErrorResourceNotFound`, `ClientResourceConflict`, +`ServerNotImplemented`, `ServerInternalError`, and `ServerErrorBadGateway`. These are API envelope +values, not a promise that a service always chooses a particular numeric HTTP code. + +## Representation and wire behavior + +- `DppRepresentation.COMPRESSED` (the default) adds no query parameter. `DppRepresentation.FULL` + sends `?representation=full`; the history route appends it after `date`. +- Request bodies are serialized from JSON-compatible Python values; non-finite numbers are + rejected before the request is sent. The response `payload` is returned as plain JSON, without + decoding into a model. +- Passport IDs and element paths are percent-encoded as path segments. +- A history `date` must be timezone-aware; the client converts it to UTC. +- The client forwards PATCH bodies unchanged. It does not implement JSON Merge Patch or validate + element paths; how a PATCH is applied is up to the service. + +## Pagination and registry metadata + +`read_dpp_ids_by_product_ids(product_ids, limit=None, cursor=None)` sends +`{"productIdentifiers": [...], "limit": null, "cursor": null}` when optional arguments are +omitted. Its `ReadDppIdsResponse` requires `dppIdentifiers` and may contain `nextCursor`. Pass that +cursor on a later call to request the next page; the client has no iterator or automatic traversal. + +`RegisterDppRequest` contains optional `unique_product_identifier`, +`digital_product_passport_id`, `unique_economic_operator_identifier`, and `dpp_api_endpoint` fields. +The request is registry metadata: it does not upload a passport. Registration may return an optional +`registration_id`; there is no public registry read-back or delete method in this Python client. + +## Decode a response into a typed passport + +The clients return plain JSON and the codecs read JSON strings, so convert between them with the +`json` module. Request `FULL`, because `from_json()` reads the FULL form: + +```python +import json + +from dpp_sdk import Dpp4FunJsonCodec +from dpp_sdk.clients import DppRepoClient, DppRepresentation +from dpp_sdk.dpp4fun import Nameplate + +codec = Dpp4FunJsonCodec() +with DppRepoClient("https://repo.example.com") as repo: + response = repo.read_dpp_by_id("urn:example:dpp:1", DppRepresentation.FULL) + +passport = codec.from_json(json.dumps(response.payload)) # or from_json_and_validate() +nameplate = passport.typed_aspect(Nameplate) +``` + +For a COMPRESSED payload use `codec.from_compressed_json(json.dumps(response.payload))`; the DPP4Fun +profile restores the types. To send a typed passport, go the other way: +`repo.create_dpp(json.loads(codec.to_json(passport)), DppRepresentation.FULL)`. + +The [full lifecycle example](../examples/consumer_lifecycle.py) builds, validates, creates, reads, +updates, reads history, registers, and cleans up a passport. There is no registry cleanup method, +so run it against a disposable service. + +## Known limitations + +- No authentication, retries, automatic pagination, or async client. +- No registry read-back or delete. +- No JSON Merge Patch or JSONPath implementation; PATCH semantics belong to the service. +- No validation: validate passports with the model validators before sending them. +- Mock-service `/internal/...` routes are not client operations. + diff --git a/docs/conformance.md b/docs/conformance.md new file mode 100644 index 0000000..60f1262 --- /dev/null +++ b/docs/conformance.md @@ -0,0 +1,51 @@ +# Standards alignment and validation rules + +This page records which standard clauses the SDK follows, where the sources disagree and which +choice the SDK makes, and which validator enforces which rule family. It is reference material for +reviewers and integrators who need to trace behavior to a source. For how to use the models, see +the [model guide](model-guide.md). + +This is a scoped alignment. It is not a formal conformance, certification, or legal-compliance +claim. + +## Source alignment and source conflicts + +Core model and JSON rules are aligned to selected requirements in EN 18223:2026 Clauses +4.1.2.1–4.1.2.9 and 5.2.2–5.2.9. EN 18223:2026 Clause 4.1.1 gives the prose and tables +precedence over conflicting UML/JSON representations. Clause 4.1.2.6 prose requires at least one +item while its table says `[0..*]`; this conflict remains recorded and validation/serialization +enforce the selected prose requirement. API client routes, fields, representations, and response +handling are traced to EN 18222:2026 Clauses 4.2–4.8, 5.2, 6.2–6.3, 7.2, and 8.1–8.4. D4.1 +profile rules are traced to v1.0 Sections 3.2.1–3.2.2 and the packaged source-backed catalog. This +is a scoped alignment; it is not a formal conformance or certification claim. + +The D4.1 catalog records source conflicts. The six Characteristics AAS templates and populated +examples define descriptive String `Surface`, while D4.1 prose describes numeric total area. The +profile follows the published AAS `xsd:string` binding without an inferred area field or semantic +IRI. `professionalUseOnly` remains Boolean per its published AAS binding despite conflicting D4.1 +prose. `ComplianceStatus` remains String per its D4.1 and AAS binding. These values are not coerced +at the DPP4Fun boundary. Identifier validation covers the supported EN 18219:2026 methods through +local syntax/profile checks only, not identifier issuance, global uniqueness, or +external resolution, and unsupported schemes remain unsupported. + +## Core rules + +| Validator | Rule families | Evidence | +| --- | --- | --- | +| `DppValidator` | Required DPP/header identifiers and metadata, DataElement identity and cardinality, kind/datatype consistency, semantic references, and nested structural consistency. | `tests/test_core_validation_parity.py`; EN 18223 Clauses 4.1.2.3–4.1.2.9. | +| `DataFormatValidator` | EN JSON-native value types, language/resource syntax, URI syntax, and list structure. | `tests/test_core_validation_parity.py`; EN 18223. | +| `IdentifierValidator` | Explicit caller-selected supported EN 18219 identifier profiles and granularity behavior. It does not infer schemes from arbitrary identifier text or prove issuance, uniqueness, or resolution. | `tests/test_core_validation_parity.py`; EN 18219:2026 clauses 4.1–4.6, 5.1–5.6, 6.1–6.5 and Annexes; only the locally implemented methods are covered. | + +## DPP4Fun rules + +| Validator | Rule and null behavior | Error path / evidence | +| --- | --- | --- | +| `Dpp4FunValidationService` | Ordered composition: core semantics, generic data-format rules, optional caller-context identifier rules, then D4.1 profile rules. Returns `ValidationResult`; `None` produces `DPP4FUN_REQUIRED`. | Core paths, then catalog paths such as `elements.nameplate.gtinCode`; `test_dpp4fun_validation.py`. | +| D4.1 content profile | Unsupported IDs produce `DPP4FUN_CONTENT_SPEC_SUPPORTED`; general aspects require the common binding; at most one Characteristics content profile can be selected. | `contentSpecificationIds`; aggregate construction rejects multiple variants, validator reports unsupported/missing bindings. | +| Catalog structural bindings | Catalogued kinds, element IDs, dictionary references, and resolved datatypes must match. Wrong scalar JSON types, malformed `xsd:anyURI`, and known wrong-dimension unit symbols produce profile findings. | `elements....`; exact stable IDs in `test_dpp4fun_final_parity.py`. | +| Catalog collections | Empty profile containers are omitted; source cardinality and binding occurrence are checked where present; repeated items use their catalogued kind and indexed ID policy. | `elements..[index]`; profile-driven tests and Java catalog validator evidence. | +| Instruction minima | Each Instruction item requires `name`, `description`, and `reference`. | `DPP4FUN_INSTRUCTION_*_REQUIRED`; item field paths. | +| Assessment minima | Certificate, StandardAssessment, Scoring, and Indicator items require `name` and `description`; CarbonFootprintCalculation has no inferred assessment minima. | `DPP4FUN_ASSESSMENT_*_REQUIRED`; item field paths. | +| Record minima | Source-defined Record items require name, description, operator, one time field, and subtype status where present. RepairRecord has no status field. | `DPP4FUN_RECORD_*_REQUIRED`; item field paths. | +| Source-conflicted values | `Surface` uses the published Characteristics AAS descriptive `xsd:string` binding with no inferred area field. Safety `professionalUseOnly` remains Boolean. `ComplianceStatus` remains String. The profile does not coerce these values; source disagreements remain recorded as profile evidence. | Catalog datatype checks; source conflicts are metadata, not runtime warnings. | + diff --git a/docs/diagram-evidence.md b/docs/diagram-evidence.md deleted file mode 100644 index 0163129..0000000 --- a/docs/diagram-evidence.md +++ /dev/null @@ -1,46 +0,0 @@ -# Python SDK diagram publication evidence - -## Purpose and convention - -Every consumer-facing diagram keeps an editable Mermaid source and a committed SVG rendered with -the pinned Mermaid CLI in [`../scripts/render_diagrams.ps1`](../scripts/render_diagrams.ps1). -Markdown publishes the SVG -with a repository-relative forward-slash path, alt text, and a caption. Do not embed a Mermaid or -Draw.io source as an image, and do not rely solely on GitLab Mermaid rendering. - -## Render and verify - -**Purpose:** generate or verify all Mermaid source/render pairs. **Run from:** repository root. -**Prerequisites:** Docker can pull the pinned -`ghcr.io/mermaid-js/mermaid-cli/mermaid-cli:11.4.2` image with digest -`sha256:99c983b3ab4e14033f2880bc1b9de17e5090b4515dabd63fe9cf8c0ae6130956`. - -### PowerShell - -```powershell -powershell -ExecutionPolicy Bypass -File scripts/render_diagrams.ps1 -powershell -ExecutionPolicy Bypass -File scripts/render_diagrams.ps1 -VerifyOnly -``` - -### Linux/macOS - -```bash -pwsh -ExecutionPolicy Bypass -File scripts/render_diagrams.ps1 -pwsh -ExecutionPolicy Bypass -File scripts/render_diagrams.ps1 -VerifyOnly -``` - -**Expected result:** each `.mmd` has a sibling `.svg`; `-VerifyOnly` does not generate assets. -**Cleanup:** none. Review regenerated SVG diffs before adding them to a change. - -## Consumer diagram inventory - -| ID | Editable source | Rendered asset | References | Source evidence | -|---|---|---|---|---| -| Python SDK overview | [`python-sdk-overview.mmd`](architecture/python-sdk-overview.mmd) | [`python-sdk-overview.svg`](architecture/python-sdk-overview.svg) | Root README and `docs/overview.md` | Current package exports and import direction | -| Client request flow | [`python-client-request-flow.mmd`](architecture/python-client-request-flow.mmd) | [`python-client-request-flow.svg`](architecture/python-client-request-flow.svg) | Clients README | Public `DppRepoClient` and `DppRegistryClient` methods | -| Core boundary | [`python-core-model.mmd`](architecture/python-core-model.mmd) | [`python-core-model.svg`](architecture/python-core-model.svg) | Core README | Core model and validation ownership | -| DPP4Fun boundary | [`python-dpp4fun-model.mmd`](architecture/python-dpp4fun-model.mmd) | [`python-dpp4fun-model.svg`](architecture/python-dpp4fun-model.svg) | DPP4Fun README | Aggregate, validation, and transport ownership | - -The editable sources and rendered SVGs use safe lowercase hyphenated names without spaces or URL -encoding. Architecture evidence not referenced by maintained Python consumer documentation is -outside this publication inventory. diff --git a/docs/dpp4fun-fields.md b/docs/dpp4fun-fields.md new file mode 100644 index 0000000..403874e --- /dev/null +++ b/docs/dpp4fun-fields.md @@ -0,0 +1,667 @@ +# DPP4Fun field reference + + + +This page lists every field of the DPP4Fun furniture profile that `dpp_sdk.dpp4fun` knows. +Use it to find the names you pass to the typed classes. + +How to read the tables: + +- **Field** is the path inside the aspect. Pass the first segment as a keyword to + `Aspect.create(...)` or as the name to `with_field()`, `field()`, and `quantity()`. Names are the + camelCase element IDs used in JSON, for example `Nameplate.create(manufacturerName="...")`. + An unknown name raises `KeyError`. +- A path with `[*]` is one item of a repeated list. Build items with the composite class shown in + the table and pass their elements as a tuple, for example + `BillOfMaterials.create(materialSet=(Material.create(name="Oak").element,))`. +- Dotted paths without `[*]` are nested collections; build them with the composite class shown + (for example `Contact`, `Address`). +- **Unit** names the unit dimension. Set such values with + `with_quantity(name, Dpp4FunQuantity(value=..., unit=...))`; the matching `...Unit` field is + filled for you. +- **Occurs** is how often the field may appear (`0..1` optional, `0..*` any number). + +All fields are optional at construction. Run `validate_dpp4fun()` to check the profile rules. + +## General aspects + +### BillOfMaterials (`billOfMaterials`) + +Build with `BillOfMaterials.create(...)`. + +| Field | Kind | Datatype | Unit | Occurs | Build with | +| --- | --- | --- | --- | --- | --- | +| `materialSet` | list | | | 0..1 | | +| `materialSet[*]` | collection | | | 0..* | `Material` | +| `materialSet[*].name` | single value | xsd:string | | 0..1 | | +| `materialSet[*].mandatory` | single value | xsd:boolean | | 0..1 | | +| `materialSet[*].portion` | single value | xsd:double | | 0..1 | | +| `materialSet[*].reference` | single value | xsd:anyURI | | 0..1 | | +| `componentSet` | list | | | 0..1 | | +| `componentSet[*]` | collection | | | 0..* | `Component` | +| `componentSet[*].name` | single value | xsd:string | | 0..1 | | +| `componentSet[*].mandatory` | single value | xsd:boolean | | 0..1 | | +| `componentSet[*].reference` | single value | xsd:anyURI | | 0..1 | | +| `partSet` | list | | | 0..1 | | +| `partSet[*]` | collection | | | 0..* | `Part` | +| `partSet[*].name` | single value | xsd:string | | 0..1 | | +| `partSet[*].mandatory` | single value | xsd:boolean | | 0..1 | | +| `partSet[*].reference` | single value | xsd:anyURI | | 0..1 | | + +### CarbonFootprint (`carbonFootprint`) + +Build with `CarbonFootprint.create(...)`. + +| Field | Kind | Datatype | Unit | Occurs | Build with | +| --- | --- | --- | --- | --- | --- | +| `carbonFootprintCalculationSet` | list | | | 0..1 | | +| `carbonFootprintCalculationSet[*]` | collection | | | 0..* | `CarbonFootprintCalculation` | +| `carbonFootprintCalculationSet[*].reference` | single value | xsd:anyURI | | 0..1 | | +| `carbonFootprintCalculationSet[*].co2eq` | single value | xsd:double | | 0..1 | | +| `carbonFootprintCalculationSet[*].phase` | single value | xsd:string | | 0..1 | | +| `carbonFootprintCalculationSet[*].quantityOfMeasurementForCalculation` | single value | xsd:string | | 0..1 | | +| `carbonFootprintCalculationSet[*].impactUnitForCalculation` | single value | xsd:string | | 0..1 | | + +### Durability (`durability`) + +Build with `Durability.create(...)`. + +| Field | Kind | Datatype | Unit | Occurs | Build with | +| --- | --- | --- | --- | --- | --- | +| `cycleLife` | single value | xsd:string | | 0..1 | | +| `expectedLifetime` | single value | xsd:string | | 0..1 | | +| `certificateSet` | list | | | 0..1 | | +| `certificateSet[*]` | collection | | | 0..* | `Certificate` | +| `certificateSet[*].name` | single value | xsd:string | | 0..1 | | +| `certificateSet[*].issueDate` | single value | xsd:dateTime | | 0..1 | | +| `certificateSet[*].description` | single value | xsd:string | | 0..1 | | +| `certificateSet[*].validityPolicy` | single value | xsd:string | | 0..1 | | +| `certificateSet[*].reference` | single value | xsd:anyURI | | 0..1 | | +| `standardSet` | list | | | 0..1 | | +| `standardSet[*]` | collection | | | 0..* | `StandardAssessment` | +| `standardSet[*].name` | single value | xsd:string | | 0..1 | | +| `standardSet[*].description` | single value | xsd:string | | 0..1 | | +| `standardSet[*].complianceStatus` | single value | xsd:string | | 0..1 | | +| `standardSet[*].status` | single value | xsd:string | | 0..1 | | +| `standardSet[*].version` | single value | xsd:string | | 0..1 | | +| `standardSet[*].reference` | single value | xsd:anyURI | | 0..1 | | +| `scoringSet` | list | | | 0..1 | | +| `scoringSet[*]` | collection | | | 0..* | `Scoring` | +| `scoringSet[*].name` | single value | xsd:string | | 0..1 | | +| `scoringSet[*].description` | single value | xsd:string | | 0..1 | | +| `scoringSet[*].value` | single value | xsd:string | | 0..1 | | +| `scoringSet[*].reference` | single value | xsd:anyURI | | 0..1 | | + +### EnvironmentalImpact (`environmentalImpact`) + +Build with `EnvironmentalImpact.create(...)`. + +| Field | Kind | Datatype | Unit | Occurs | Build with | +| --- | --- | --- | --- | --- | --- | +| `indicatorSet` | list | | | 0..1 | | +| `indicatorSet[*]` | collection | | | 0..* | | +| `indicatorSet[*].name` | single value | xsd:string | | 0..1 | | +| `indicatorSet[*].description` | single value | xsd:string | | 0..1 | | +| `indicatorSet[*].value` | single value | xsd:string | | 0..1 | | +| `indicatorSet[*].unit` | single value | xsd:string | | 0..1 | | +| `indicatorSet[*].calculationMethod` | single value | xsd:anyURI | | 0..1 | | + +### HealthImpact (`healthImpact`) + +Build with `HealthImpact.create(...)`. + +| Field | Kind | Datatype | Unit | Occurs | Build with | +| --- | --- | --- | --- | --- | --- | +| `indicatorSet` | list | | | 0..1 | | +| `indicatorSet[*]` | collection | | | 0..* | `HealthIndicator` / `Indicator` | +| `indicatorSet[*].name` | single value | xsd:string | | 0..1 | | +| `indicatorSet[*].description` | single value | xsd:string | | 0..1 | | +| `indicatorSet[*].value` | single value | xsd:string | | 0..1 | | +| `indicatorSet[*].unit` | single value | xsd:string | | 0..1 | | +| `indicatorSet[*].calculationMethod` | single value | xsd:anyURI | | 0..1 | | +| `indicatorSet[*].hazardType` | single value | xsd:string | | 0..1 | | + +### Nameplate (`nameplate`) + +Build with `Nameplate.create(...)`. + +| Field | Kind | Datatype | Unit | Occurs | Build with | +| --- | --- | --- | --- | --- | --- | +| `gtinCode` | single value | xsd:string | | 0..1 | | +| `internalArticleNumber` | single value | xsd:string | | 0..1 | | +| `batchNumber` | single value | xsd:string | | 0..1 | | +| `manufacturerName` | single value | xsd:string | | 0..1 | | +| `manufacturerProductDescription` | multi-language text | | | 0..1 | | +| `manufacturerProductDesignation` | multi-language text | | | 0..1 | | +| `manufacturerProductFamily` | multi-language text | | | 0..1 | | +| `manufacturerProductRoot` | multi-language text | | | 0..1 | | +| `manufacturerProductOrderSuffix` | multi-language text | | | 0..1 | | +| `uriOfManufacturer` | single value | xsd:anyURI | | 0..1 | | +| `glnOfManufacturer` | single value | xsd:string | | 0..1 | | +| `hsCodeOfTheWCO` | single value | xsd:string | | 0..1 | | +| `customsTariffNumber` | single value | xsd:string | | 0..1 | | +| `brand` | single value | xsd:string | | 0..1 | | +| `productType` | multi-language text | | | 0..1 | | +| `uriOfTheProduct` | single value | xsd:anyURI | | 0..1 | | +| `productArticleNumberOfSupplier` | single value | xsd:string | | 0..1 | | +| `nameOfSupplier` | single value | xsd:string | | 0..1 | | +| `supplierProductDescription` | multi-language text | | | 0..1 | | +| `supplierProductDesignation` | multi-language text | | | 0..1 | | +| `supplierProductFamily` | multi-language text | | | 0..1 | | +| `supplierProductRoot` | multi-language text | | | 0..1 | | +| `supplierProductOrderSuffix` | multi-language text | | | 0..1 | | +| `supplierProductType` | multi-language text | | | 0..1 | | +| `uriOfSupplier` | single value | xsd:anyURI | | 0..1 | | +| `glnOfSupplier` | single value | xsd:string | | 0..1 | | +| `addressOfAdditionalLink` | single value | xsd:anyURI | | 0..1 | | +| `contact` | collection | | | 0..1 | `Contact` | +| `contact.organization` | single value | xsd:string | | 0..1 | | +| `contact.address` | collection | | | 0..1 | `Address` | +| `contact.address.country` | single value | xsd:string | | 0..1 | | +| `contact.address.zipCode` | single value | xsd:string | | 0..1 | | +| `contact.address.region` | single value | xsd:string | | 0..1 | | +| `contact.address.town` | single value | xsd:string | | 0..1 | | +| `contact.address.street` | single value | xsd:string | | 0..1 | | +| `contact.telephone` | collection | | | 0..1 | `Telephone` | +| `contact.telephone.telephoneNumber` | single value | xsd:string | | 0..1 | | +| `contact.telephone.typeOfTelephone` | single value | xsd:string | | 0..1 | | +| `contact.email` | collection | | | 0..1 | `Email` | +| `contact.email.emailAddress` | single value | xsd:string | | 0..1 | | +| `contact.email.typeOfEmail` | single value | xsd:string | | 0..1 | | + +### ProductRelationship (`productRelationship`) + +Build with `ProductRelationship.create(...)`. + +| Field | Kind | Datatype | Unit | Occurs | Build with | +| --- | --- | --- | --- | --- | --- | +| `accessorySet` | list | | | 0..1 | | +| `accessorySet[*]` | collection | | | 0..* | `Accessory` | +| `accessorySet[*].name` | single value | xsd:string | | 0..1 | | +| `accessorySet[*].description` | single value | xsd:string | | 0..1 | | +| `accessorySet[*].reference` | single value | xsd:anyURI | | 0..1 | | +| `collectionMemberSet` | list | | | 0..1 | | +| `collectionMemberSet[*]` | collection | | | 0..* | `CollectionMember` | +| `collectionMemberSet[*].name` | single value | xsd:string | | 0..1 | | +| `collectionMemberSet[*].description` | single value | xsd:string | | 0..1 | | +| `collectionMemberSet[*].collection` | single value | xsd:string | | 0..1 | | +| `collectionMemberSet[*].reference` | single value | xsd:anyURI | | 0..1 | | + +### Recyclability (`recyclability`) + +Build with `Recyclability.create(...)`. + +| Field | Kind | Datatype | Unit | Occurs | Build with | +| --- | --- | --- | --- | --- | --- | +| `recycledContent` | single value | xsd:double | | 0..1 | | +| `recyclable` | single value | xsd:boolean | | 0..1 | | +| `recycledMaterialIncluded` | single value | xsd:boolean | | 0..1 | | +| `certificateSet` | list | | | 0..1 | | +| `certificateSet[*]` | collection | | | 0..* | | +| `certificateSet[*].name` | single value | xsd:string | | 0..1 | | +| `certificateSet[*].issueDate` | single value | xsd:dateTime | | 0..1 | | +| `certificateSet[*].description` | single value | xsd:string | | 0..1 | | +| `certificateSet[*].validityPolicy` | single value | xsd:string | | 0..1 | | +| `certificateSet[*].reference` | single value | xsd:anyURI | | 0..1 | | +| `standardSet` | list | | | 0..1 | | +| `standardSet[*]` | collection | | | 0..* | | +| `standardSet[*].name` | single value | xsd:string | | 0..1 | | +| `standardSet[*].description` | single value | xsd:string | | 0..1 | | +| `standardSet[*].complianceStatus` | single value | xsd:string | | 0..1 | | +| `standardSet[*].status` | single value | xsd:string | | 0..1 | | +| `standardSet[*].version` | single value | xsd:string | | 0..1 | | +| `standardSet[*].reference` | single value | xsd:anyURI | | 0..1 | | +| `recyclingInstructionSet` | list | | | 0..1 | | +| `recyclingInstructionSet[*]` | collection | | | 0..* | | +| `recyclingInstructionSet[*].name` | single value | xsd:string | | 0..1 | | +| `recyclingInstructionSet[*].description` | single value | xsd:string | | 0..1 | | +| `recyclingInstructionSet[*].reference` | single value | xsd:anyURI | | 0..1 | | + +### Refurbishment (`refurbishment`) + +Build with `Refurbishment.create(...)`. + +| Field | Kind | Datatype | Unit | Occurs | Build with | +| --- | --- | --- | --- | --- | --- | +| `refurbishmentInstructionSet` | list | | | 0..1 | | +| `refurbishmentInstructionSet[*]` | collection | | | 0..* | | +| `refurbishmentInstructionSet[*].name` | single value | xsd:string | | 0..1 | | +| `refurbishmentInstructionSet[*].description` | single value | xsd:string | | 0..1 | | +| `refurbishmentInstructionSet[*].reference` | single value | xsd:anyURI | | 0..1 | | + +### Reliability (`reliability`) + +Build with `Reliability.create(...)`. + +| Field | Kind | Datatype | Unit | Occurs | Build with | +| --- | --- | --- | --- | --- | --- | +| `certificateSet` | list | | | 0..1 | | +| `certificateSet[*]` | collection | | | 0..* | | +| `certificateSet[*].name` | single value | xsd:string | | 0..1 | | +| `certificateSet[*].issueDate` | single value | xsd:dateTime | | 0..1 | | +| `certificateSet[*].description` | single value | xsd:string | | 0..1 | | +| `certificateSet[*].validityPolicy` | single value | xsd:string | | 0..1 | | +| `certificateSet[*].reference` | single value | xsd:anyURI | | 0..1 | | +| `standardSet` | list | | | 0..1 | | +| `standardSet[*]` | collection | | | 0..* | | +| `standardSet[*].name` | single value | xsd:string | | 0..1 | | +| `standardSet[*].description` | single value | xsd:string | | 0..1 | | +| `standardSet[*].complianceStatus` | single value | xsd:string | | 0..1 | | +| `standardSet[*].status` | single value | xsd:string | | 0..1 | | +| `standardSet[*].version` | single value | xsd:string | | 0..1 | | +| `standardSet[*].reference` | single value | xsd:anyURI | | 0..1 | | +| `scoringSet` | list | | | 0..1 | | +| `scoringSet[*]` | collection | | | 0..* | | +| `scoringSet[*].name` | single value | xsd:string | | 0..1 | | +| `scoringSet[*].description` | single value | xsd:string | | 0..1 | | +| `scoringSet[*].value` | single value | xsd:string | | 0..1 | | +| `scoringSet[*].reference` | single value | xsd:anyURI | | 0..1 | | + +### Remanufacturing (`remanufacturing`) + +Build with `Remanufacturing.create(...)`. + +| Field | Kind | Datatype | Unit | Occurs | Build with | +| --- | --- | --- | --- | --- | --- | +| `assemblyInstructionSet` | list | | | 0..1 | | +| `assemblyInstructionSet[*]` | collection | | | 0..* | | +| `assemblyInstructionSet[*].name` | single value | xsd:string | | 0..1 | | +| `assemblyInstructionSet[*].description` | single value | xsd:string | | 0..1 | | +| `assemblyInstructionSet[*].targetAudience` | single value | xsd:string | | 0..1 | | +| `assemblyInstructionSet[*].reference` | single value | xsd:anyURI | | 0..1 | | +| `disassemblyInstructionSet` | list | | | 0..1 | | +| `disassemblyInstructionSet[*]` | collection | | | 0..* | | +| `disassemblyInstructionSet[*].name` | single value | xsd:string | | 0..1 | | +| `disassemblyInstructionSet[*].description` | single value | xsd:string | | 0..1 | | +| `disassemblyInstructionSet[*].targetAudience` | single value | xsd:string | | 0..1 | | +| `disassemblyInstructionSet[*].reference` | single value | xsd:anyURI | | 0..1 | | + +### Repairability (`repairability`) + +Build with `Repairability.create(...)`. + +| Field | Kind | Datatype | Unit | Occurs | Build with | +| --- | --- | --- | --- | --- | --- | +| `sparePartsAvailable` | single value | xsd:boolean | | 0..1 | | +| `repairInstructionSet` | list | | | 0..1 | | +| `repairInstructionSet[*]` | collection | | | 0..* | | +| `repairInstructionSet[*].name` | single value | xsd:string | | 0..1 | | +| `repairInstructionSet[*].description` | single value | xsd:string | | 0..1 | | +| `repairInstructionSet[*].targetAudience` | single value | xsd:string | | 0..1 | | +| `repairInstructionSet[*].reference` | single value | xsd:anyURI | | 0..1 | | +| `repairRecordSet` | list | | | 0..1 | | +| `repairRecordSet[*]` | collection | | | 0..* | `RepairRecord` | +| `repairRecordSet[*].name` | single value | xsd:string | | 0..1 | | +| `repairRecordSet[*].description` | single value | xsd:string | | 0..1 | | +| `repairRecordSet[*].operator` | single value | xsd:string | | 0..1 | | +| `repairRecordSet[*].time` | single value | xsd:string | | 0..1 | | +| `repairRecordSet[*].location` | single value | xsd:string | | 0..1 | | +| `repairRecordSet[*].type` | single value | xsd:string | | 0..1 | | + +### Reusability (`reusability`) + +Build with `Reusability.create(...)`. + +| Field | Kind | Datatype | Unit | Occurs | Build with | +| --- | --- | --- | --- | --- | --- | +| `secondHand` | single value | xsd:boolean | | 0..1 | | +| `scoringSet` | list | | | 0..1 | | +| `scoringSet[*]` | collection | | | 0..* | | +| `scoringSet[*].name` | single value | xsd:string | | 0..1 | | +| `scoringSet[*].description` | single value | xsd:string | | 0..1 | | +| `scoringSet[*].value` | single value | xsd:string | | 0..1 | | +| `scoringSet[*].reference` | single value | xsd:anyURI | | 0..1 | | +| `scoringInstructionSet` | list | | | 0..1 | | +| `scoringInstructionSet[*]` | collection | | | 0..* | | +| `scoringInstructionSet[*].name` | single value | xsd:string | | 0..1 | | +| `scoringInstructionSet[*].description` | single value | xsd:string | | 0..1 | | +| `scoringInstructionSet[*].reference` | single value | xsd:anyURI | | 0..1 | | + +### RightOfEndUsers (`rightOfEndUsers`) + +Build with `RightOfEndUsers.create(...)`. + +| Field | Kind | Datatype | Unit | Occurs | Build with | +| --- | --- | --- | --- | --- | --- | +| `postSaleServiceSet` | list | | | 0..1 | | +| `postSaleServiceSet[*]` | collection | | | 0..* | `PostSaleService` | +| `postSaleServiceSet[*].name` | single value | xsd:string | | 0..1 | | +| `postSaleServiceSet[*].description` | single value | xsd:string | | 0..1 | | +| `postSaleServiceSet[*].reference` | single value | xsd:anyURI | | 0..1 | | +| `policies` | collection | | | 0..1 | `Policies` | +| `policies.prohibitionSet` | list | | | 0..1 | | +| `policies.prohibitionSet[*]` | single value | xsd:string | | 0..* | | +| `policies.permissionSet` | list | | | 0..1 | | +| `policies.permissionSet[*]` | single value | xsd:string | | 0..* | | + +### Safety (`safety`) + +Build with `Safety.create(...)`. + +| Field | Kind | Datatype | Unit | Occurs | Build with | +| --- | --- | --- | --- | --- | --- | +| `ageRestriction` | single value | xsd:string | | 0..1 | | +| `professionalUseOnly` | single value | xsd:boolean | | 0..1 | | +| `certificateSet` | list | | | 0..1 | | +| `certificateSet[*]` | collection | | | 0..* | | +| `certificateSet[*].name` | single value | xsd:string | | 0..1 | | +| `certificateSet[*].issueDate` | single value | xsd:dateTime | | 0..1 | | +| `certificateSet[*].description` | single value | xsd:string | | 0..1 | | +| `certificateSet[*].validityPolicy` | single value | xsd:string | | 0..1 | | +| `certificateSet[*].reference` | single value | xsd:anyURI | | 0..1 | | +| `standardSet` | list | | | 0..1 | | +| `standardSet[*]` | collection | | | 0..* | | +| `standardSet[*].name` | single value | xsd:string | | 0..1 | | +| `standardSet[*].description` | single value | xsd:string | | 0..1 | | +| `standardSet[*].complianceStatus` | single value | xsd:string | | 0..1 | | +| `standardSet[*].status` | single value | xsd:string | | 0..1 | | +| `standardSet[*].version` | single value | xsd:string | | 0..1 | | +| `standardSet[*].reference` | single value | xsd:anyURI | | 0..1 | | + +### Traceability (`traceability`) + +Build with `Traceability.create(...)`. + +| Field | Kind | Datatype | Unit | Occurs | Build with | +| --- | --- | --- | --- | --- | --- | +| `currentPhase` | single value | xsd:string | | 0..1 | | +| `location` | single value | xsd:string | | 0..1 | | +| `designRecordSet` | list | | | 0..1 | | +| `designRecordSet[*]` | collection | | | 0..* | `DesignRecord` | +| `designRecordSet[*].name` | single value | xsd:string | | 0..1 | | +| `designRecordSet[*].description` | single value | xsd:string | | 0..1 | | +| `designRecordSet[*].operator` | single value | xsd:string | | 0..1 | | +| `designRecordSet[*].time` | single value | xsd:dateTime | | 0..1 | | +| `designRecordSet[*].version` | single value | xsd:string | | 0..1 | | +| `designRecordSet[*].status` | single value | xsd:string | | 0..1 | | +| `designRecordSet[*].designFile` | single value | xsd:anyURI | | 0..1 | | +| `manufacturingRecordSet` | list | | | 0..1 | | +| `manufacturingRecordSet[*]` | collection | | | 0..* | `ManufacturingRecord` | +| `manufacturingRecordSet[*].name` | single value | xsd:string | | 0..1 | | +| `manufacturingRecordSet[*].description` | single value | xsd:string | | 0..1 | | +| `manufacturingRecordSet[*].operator` | single value | xsd:string | | 0..1 | | +| `manufacturingRecordSet[*].startTime` | single value | xsd:dateTime | | 0..1 | | +| `manufacturingRecordSet[*].endTime` | single value | xsd:dateTime | | 0..1 | | +| `manufacturingRecordSet[*].station` | single value | xsd:string | | 0..1 | | +| `manufacturingRecordSet[*].status` | single value | xsd:string | | 0..1 | | +| `manufacturingRecordSet[*].positionInProcessRouting` | single value | xsd:anyURI | | 0..1 | | +| `trackingRecordSet` | list | | | 0..1 | | +| `trackingRecordSet[*]` | collection | | | 0..* | `TrackingRecord` | +| `trackingRecordSet[*].name` | single value | xsd:string | | 0..1 | | +| `trackingRecordSet[*].description` | single value | xsd:string | | 0..1 | | +| `trackingRecordSet[*].operator` | single value | xsd:string | | 0..1 | | +| `trackingRecordSet[*].time` | single value | xsd:dateTime | | 0..1 | | +| `trackingRecordSet[*].status` | single value | xsd:string | | 0..1 | | +| `trackingRecordSet[*].location` | single value | xsd:string | | 0..1 | | +| `trackingRecordSet[*].origin` | single value | xsd:string | | 0..1 | | +| `trackingRecordSet[*].destination` | single value | xsd:string | | 0..1 | | + +### Upgradability (`upgradability`) + +Build with `Upgradability.create(...)`. + +| Field | Kind | Datatype | Unit | Occurs | Build with | +| --- | --- | --- | --- | --- | --- | +| `upgradeOptionsAvailable` | single value | xsd:boolean | | 0..1 | | +| `upgradeInstructionSet` | list | | | 0..1 | | +| `upgradeInstructionSet[*]` | collection | | | 0..* | | +| `upgradeInstructionSet[*].name` | single value | xsd:string | | 0..1 | | +| `upgradeInstructionSet[*].description` | single value | xsd:string | | 0..1 | | +| `upgradeInstructionSet[*].reference` | single value | xsd:anyURI | | 0..1 | | + +### Usage (`usage`) + +Build with `Usage.create(...)`. + +| Field | Kind | Datatype | Unit | Occurs | Build with | +| --- | --- | --- | --- | --- | --- | +| `operatingHours` | single value | xsd:integer | | 0..1 | | +| `primaryFunction` | collection | | | 0..1 | `ProductFunction` | +| `primaryFunction.name` | single value | xsd:string | | 0..1 | | +| `primaryFunction.description` | single value | xsd:string | | 0..1 | | +| `primaryFunction.status` | single value | xsd:string | | 0..1 | | +| `secondaryFunctionSet` | list | | | 0..1 | | +| `secondaryFunctionSet[*]` | collection | | | 0..* | | +| `secondaryFunctionSet[*].name` | single value | xsd:string | | 0..1 | | +| `secondaryFunctionSet[*].description` | single value | xsd:string | | 0..1 | | +| `secondaryFunctionSet[*].status` | single value | xsd:string | | 0..1 | | +| `assemblyInstructionSet` | list | | | 0..1 | | +| `assemblyInstructionSet[*]` | collection | | | 0..* | `Instruction` | +| `assemblyInstructionSet[*].name` | single value | xsd:string | | 0..1 | | +| `assemblyInstructionSet[*].description` | single value | xsd:string | | 0..1 | | +| `assemblyInstructionSet[*].targetAudience` | single value | xsd:string | | 0..1 | | +| `assemblyInstructionSet[*].reference` | single value | xsd:anyURI | | 0..1 | | +| `disassemblyInstructionSet` | list | | | 0..1 | | +| `disassemblyInstructionSet[*]` | collection | | | 0..* | | +| `disassemblyInstructionSet[*].name` | single value | xsd:string | | 0..1 | | +| `disassemblyInstructionSet[*].description` | single value | xsd:string | | 0..1 | | +| `disassemblyInstructionSet[*].targetAudience` | single value | xsd:string | | 0..1 | | +| `disassemblyInstructionSet[*].reference` | single value | xsd:anyURI | | 0..1 | | + +### WasteManagement (`wasteManagement`) + +Build with `WasteManagement.create(...)`. + +| Field | Kind | Datatype | Unit | Occurs | Build with | +| --- | --- | --- | --- | --- | --- | +| `toxicityRisk` | single value | xsd:boolean | | 0..1 | | +| `fireRisk` | single value | xsd:boolean | | 0..1 | | +| `electricityRisk` | single value | xsd:boolean | | 0..1 | | +| `disposalInstructionSet` | list | | | 0..1 | | +| `disposalInstructionSet[*]` | collection | | | 0..* | | +| `disposalInstructionSet[*].name` | single value | xsd:string | | 0..1 | | +| `disposalInstructionSet[*].description` | single value | xsd:string | | 0..1 | | +| `disposalInstructionSet[*].reference` | single value | xsd:anyURI | | 0..1 | | +| `disassemblyInstructionSet` | list | | | 0..1 | | +| `disassemblyInstructionSet[*]` | collection | | | 0..* | | +| `disassemblyInstructionSet[*].name` | single value | xsd:string | | 0..1 | | +| `disassemblyInstructionSet[*].description` | single value | xsd:string | | 0..1 | | +| `disassemblyInstructionSet[*].targetAudience` | single value | xsd:string | | 0..1 | | +| `disassemblyInstructionSet[*].reference` | single value | xsd:anyURI | | 0..1 | | + +## Product types + +### ChairCharacteristics (`characteristics`) + +Build with `ChairCharacteristics.create(...)`. + +| Field | Kind | Datatype | Unit | Occurs | Build with | +| --- | --- | --- | --- | --- | --- | +| `length` | single value | xsd:double | length | 0..1 | | +| `width` | single value | xsd:double | length | 0..1 | | +| `height` | single value | xsd:double | length | 0..1 | | +| `diameter` | single value | xsd:double | length | 0..1 | | +| `weight` | single value | xsd:double | mass | 0..1 | | +| `volume` | single value | xsd:double | volume | 0..1 | | +| `surface` | single value | xsd:string | | 0..1 | | +| `color` | single value | xsd:string | | 0..1 | | +| `lengthUnit` | single value | xsd:string | | 0..1 | | +| `widthUnit` | single value | xsd:string | | 0..1 | | +| `heightUnit` | single value | xsd:string | | 0..1 | | +| `diameterUnit` | single value | xsd:string | | 0..1 | | +| `weightUnit` | single value | xsd:string | | 0..1 | | +| `volumeUnit` | single value | xsd:string | | 0..1 | | +| `seatHeight` | single value | xsd:double | length | 0..1 | | +| `seatWidth` | single value | xsd:double | length | 0..1 | | +| `seatLength` | single value | xsd:double | length | 0..1 | | +| `armrestHeight` | single value | xsd:double | length | 0..1 | | +| `armrestWidth` | single value | xsd:double | length | 0..1 | | +| `backrestHeight` | single value | xsd:double | length | 0..1 | | +| `legHeight` | single value | xsd:double | length | 0..1 | | +| `foamHardnessLevel` | single value | xsd:string | | 0..1 | | +| `seatingCapacity` | single value | xsd:integer | | 0..1 | | +| `indoorUse` | single value | xsd:boolean | | 0..1 | | +| `outdoorUse` | single value | xsd:boolean | | 0..1 | | +| `reclining` | single value | xsd:boolean | | 0..1 | | +| `seatSliding` | single value | xsd:boolean | | 0..1 | | +| `swivel` | single value | xsd:boolean | | 0..1 | | +| `waterRepellent` | single value | xsd:boolean | | 0..1 | | +| `stainResistant` | single value | xsd:boolean | | 0..1 | | +| `easyToClean` | single value | xsd:boolean | | 0..1 | | +| `fireResistant` | single value | xsd:boolean | | 0..1 | | +| `seatRemovableCover` | single value | xsd:boolean | | 0..1 | | +| `backrestRemovableCover` | single value | xsd:boolean | | 0..1 | | +| `adjustableFeet` | single value | xsd:boolean | | 0..1 | | +| `armrestIncluded` | single value | xsd:boolean | | 0..1 | | +| `armrestRemovableCover` | single value | xsd:boolean | | 0..1 | | +| `stackable` | single value | xsd:boolean | | 0..1 | | +| `heightAdjustable` | single value | xsd:boolean | | 0..1 | | +| `padded` | single value | xsd:boolean | | 0..1 | | +| `payload` | single value | xsd:double | mass | 0..1 | | + +### CribCharacteristics (`characteristics`) + +Build with `CribCharacteristics.create(...)`. + +| Field | Kind | Datatype | Unit | Occurs | Build with | +| --- | --- | --- | --- | --- | --- | +| `length` | single value | xsd:double | length | 0..1 | | +| `width` | single value | xsd:double | length | 0..1 | | +| `height` | single value | xsd:double | length | 0..1 | | +| `diameter` | single value | xsd:double | length | 0..1 | | +| `weight` | single value | xsd:double | mass | 0..1 | | +| `volume` | single value | xsd:double | volume | 0..1 | | +| `surface` | single value | xsd:string | | 0..1 | | +| `color` | single value | xsd:string | | 0..1 | | +| `lengthUnit` | single value | xsd:string | | 0..1 | | +| `widthUnit` | single value | xsd:string | | 0..1 | | +| `heightUnit` | single value | xsd:string | | 0..1 | | +| `diameterUnit` | single value | xsd:string | | 0..1 | | +| `weightUnit` | single value | xsd:string | | 0..1 | | +| `volumeUnit` | single value | xsd:string | | 0..1 | | +| `payload` | single value | xsd:double | mass | 0..1 | | +| `internalLength` | single value | xsd:double | length | 0..1 | | +| `internalWidth` | single value | xsd:double | length | 0..1 | | +| `matelasIncluded` | single value | xsd:boolean | | 0..1 | | + +### FabricCharacteristics (`characteristics`) + +Build with `FabricCharacteristics.create(...)`. + +| Field | Kind | Datatype | Unit | Occurs | Build with | +| --- | --- | --- | --- | --- | --- | +| `length` | single value | xsd:double | length | 0..1 | | +| `width` | single value | xsd:double | length | 0..1 | | +| `height` | single value | xsd:double | length | 0..1 | | +| `diameter` | single value | xsd:double | length | 0..1 | | +| `weight` | single value | xsd:double | mass | 0..1 | | +| `volume` | single value | xsd:double | volume | 0..1 | | +| `surface` | single value | xsd:string | | 0..1 | | +| `color` | single value | xsd:string | | 0..1 | | +| `lengthUnit` | single value | xsd:string | | 0..1 | | +| `widthUnit` | single value | xsd:string | | 0..1 | | +| `heightUnit` | single value | xsd:string | | 0..1 | | +| `diameterUnit` | single value | xsd:string | | 0..1 | | +| `weightUnit` | single value | xsd:string | | 0..1 | | +| `volumeUnit` | single value | xsd:string | | 0..1 | | +| `lightFastness` | single value | xsd:string | | 0..1 | | +| `abrasionResistance` | single value | xsd:string | | 0..1 | | +| `pilling` | single value | xsd:string | | 0..1 | | +| `seamSlippage` | single value | xsd:string | | 0..1 | | +| `flameRetardant` | single value | xsd:boolean | | 0..1 | | +| `colorFastness` | single value | xsd:integer | | 0..1 | | +| `texture` | single value | xsd:string | | 0..1 | | + +### LockerCharacteristics (`characteristics`) + +Build with `LockerCharacteristics.create(...)`. + +| Field | Kind | Datatype | Unit | Occurs | Build with | +| --- | --- | --- | --- | --- | --- | +| `length` | single value | xsd:double | length | 0..1 | | +| `width` | single value | xsd:double | length | 0..1 | | +| `height` | single value | xsd:double | length | 0..1 | | +| `diameter` | single value | xsd:double | length | 0..1 | | +| `weight` | single value | xsd:double | mass | 0..1 | | +| `volume` | single value | xsd:double | volume | 0..1 | | +| `surface` | single value | xsd:string | | 0..1 | | +| `color` | single value | xsd:string | | 0..1 | | +| `lengthUnit` | single value | xsd:string | | 0..1 | | +| `widthUnit` | single value | xsd:string | | 0..1 | | +| `heightUnit` | single value | xsd:string | | 0..1 | | +| `diameterUnit` | single value | xsd:string | | 0..1 | | +| `weightUnit` | single value | xsd:string | | 0..1 | | +| `volumeUnit` | single value | xsd:string | | 0..1 | | +| `valvePayload` | single value | xsd:double | mass | 0..1 | | +| `wallThickness` | single value | xsd:double | length | 0..1 | | +| `lockIncluded` | single value | xsd:boolean | | 0..1 | | + +### ParticleBoardCharacteristics (`characteristics`) + +Build with `ParticleBoardCharacteristics.create(...)`. + +| Field | Kind | Datatype | Unit | Occurs | Build with | +| --- | --- | --- | --- | --- | --- | +| `length` | single value | xsd:double | length | 0..1 | | +| `width` | single value | xsd:double | length | 0..1 | | +| `height` | single value | xsd:double | length | 0..1 | | +| `diameter` | single value | xsd:double | length | 0..1 | | +| `weight` | single value | xsd:double | mass | 0..1 | | +| `volume` | single value | xsd:double | volume | 0..1 | | +| `surface` | single value | xsd:string | | 0..1 | | +| `color` | single value | xsd:string | | 0..1 | | +| `lengthUnit` | single value | xsd:string | | 0..1 | | +| `widthUnit` | single value | xsd:string | | 0..1 | | +| `heightUnit` | single value | xsd:string | | 0..1 | | +| `diameterUnit` | single value | xsd:string | | 0..1 | | +| `weightUnit` | single value | xsd:string | | 0..1 | | +| `volumeUnit` | single value | xsd:string | | 0..1 | | +| `texture` | single value | xsd:string | | 0..1 | | +| `easyToClean` | single value | xsd:boolean | | 0..1 | | +| `easyToMill` | single value | xsd:boolean | | 0..1 | | +| `stainResistant` | single value | xsd:boolean | | 0..1 | | +| `scratchResistant` | single value | xsd:boolean | | 0..1 | | +| `antiBacterial` | single value | xsd:boolean | | 0..1 | | +| `carb2Epa` | single value | xsd:boolean | | 0..1 | | + +### SofaCharacteristics (`characteristics`) + +Build with `SofaCharacteristics.create(...)`. + +| Field | Kind | Datatype | Unit | Occurs | Build with | +| --- | --- | --- | --- | --- | --- | +| `length` | single value | xsd:double | length | 0..1 | | +| `width` | single value | xsd:double | length | 0..1 | | +| `height` | single value | xsd:double | length | 0..1 | | +| `diameter` | single value | xsd:double | length | 0..1 | | +| `weight` | single value | xsd:double | mass | 0..1 | | +| `volume` | single value | xsd:double | volume | 0..1 | | +| `surface` | single value | xsd:string | | 0..1 | | +| `color` | single value | xsd:string | | 0..1 | | +| `lengthUnit` | single value | xsd:string | | 0..1 | | +| `widthUnit` | single value | xsd:string | | 0..1 | | +| `heightUnit` | single value | xsd:string | | 0..1 | | +| `diameterUnit` | single value | xsd:string | | 0..1 | | +| `weightUnit` | single value | xsd:string | | 0..1 | | +| `volumeUnit` | single value | xsd:string | | 0..1 | | +| `seatHeight` | single value | xsd:double | length | 0..1 | | +| `seatWidth` | single value | xsd:double | length | 0..1 | | +| `seatLength` | single value | xsd:double | length | 0..1 | | +| `armrestHeight` | single value | xsd:double | length | 0..1 | | +| `armrestWidth` | single value | xsd:double | length | 0..1 | | +| `backrestHeight` | single value | xsd:double | length | 0..1 | | +| `legHeight` | single value | xsd:double | length | 0..1 | | +| `foamHardnessLevel` | single value | xsd:string | | 0..1 | | +| `seatingCapacity` | single value | xsd:integer | | 0..1 | | +| `indoorUse` | single value | xsd:boolean | | 0..1 | | +| `outdoorUse` | single value | xsd:boolean | | 0..1 | | +| `reclining` | single value | xsd:boolean | | 0..1 | | +| `seatSliding` | single value | xsd:boolean | | 0..1 | | +| `swivel` | single value | xsd:boolean | | 0..1 | | +| `waterRepellent` | single value | xsd:boolean | | 0..1 | | +| `stainResistant` | single value | xsd:boolean | | 0..1 | | +| `easyToClean` | single value | xsd:boolean | | 0..1 | | +| `fireResistant` | single value | xsd:boolean | | 0..1 | | +| `seatRemovableCover` | single value | xsd:boolean | | 0..1 | | +| `backrestRemovableCover` | single value | xsd:boolean | | 0..1 | | +| `adjustableFeet` | single value | xsd:boolean | | 0..1 | | +| `armrestIncluded` | single value | xsd:boolean | | 0..1 | | +| `armrestRemovableCover` | single value | xsd:boolean | | 0..1 | | +| `payload` | single value | xsd:double | mass | 0..1 | | +| `headrestIncluded` | single value | xsd:boolean | | 0..1 | | +| `additionalCushionIncluded` | single value | xsd:boolean | | 0..1 | | +| `cushionRemovableCover` | single value | xsd:boolean | | 0..1 | | +| `footrestIncluded` | single value | xsd:boolean | | 0..1 | | +| `armrestType` | single value | xsd:string | | 0..1 | | +| `numberOfCushions` | single value | xsd:integer | | 0..1 | | diff --git a/docs/extending.md b/docs/extending.md new file mode 100644 index 0000000..91a5dbf --- /dev/null +++ b/docs/extending.md @@ -0,0 +1,171 @@ +# Building your own profile + +`dpp_sdk.dpp4fun` is one profile built on `dpp_sdk.core`. You can build passports for other product +groups the same way: Core gives you the passport model, JSON conversion, and the standard +validation, and you add three things for your product group: + +1. **Element IDs and types:** which DataElements your passports contain. +2. **A semantic resolver:** where COMPRESSED JSON gets its types back from. +3. **Profile checks:** rules that apply on top of `DppValidator`. + +The code blocks on this page build on each other and run in order. They use a small battery +profile as the example. + +## 1. Build the passport from core elements + +Give your profile a content-specification ID and put it in the header. Resolvers and validators use +it to recognise which profile a passport follows. + +```python +from datetime import UTC, datetime + +from dpp_sdk.core import ( + DataElementCollection, + DigitalProductPassport, + DppHeader, + Granularity, + SingleValuedDataElement, +) + +PROFILE_ID = "urn:example:profile:battery:v1" + +passport = DigitalProductPassport( + dpp_header=DppHeader( + digital_product_passport_id="urn:example:dpp:battery-1", + unique_product_identifier="urn:example:product:battery-1", + granularity=Granularity.ITEM, + dpp_schema_version="1.0", + dpp_status="active", + last_updated=datetime(2026, 1, 1, tzinfo=UTC), + economic_operator_id="urn:example:operator:acme", + content_specification_ids=(PROFILE_ID,), + ), + data_elements=( + DataElementCollection( + element_id="battery", + dictionary_reference="https://example.com/battery#Battery", + elements=( + SingleValuedDataElement( + element_id="chemistry", + value="LFP", + value_data_type="xsd:string", + ), + SingleValuedDataElement( + element_id="capacityKwh", + value=12.5, + value_data_type="xsd:double", + ), + ), + ), + ), +) +``` + +A small helper per aspect (like `Nameplate.create()` in DPP4Fun) is usually all the typing you +need. Start without one. + +## 2. Resolve COMPRESSED JSON + +COMPRESSED JSON drops `objectType`, datatypes, and dictionary references. To read it back into a +typed passport, implement `SemanticResolver`. It is a protocol with one method: given the +passport's content-specification IDs and an element path, return a `SemanticDefinition` or `None` +for "unknown". + +```python +from collections.abc import Sequence + +from dpp_sdk.core import DataElementKind, DppJsonCodec, SemanticDefinition, SemanticPath + +BATTERY_FIELDS = { + ("battery",): SemanticDefinition( + kind=DataElementKind.COLLECTION, + element_id="battery", + dictionary_reference="https://example.com/battery#Battery", + ), + ("battery", "chemistry"): SemanticDefinition( + kind=DataElementKind.SINGLE, + element_id="chemistry", + value_data_type="xsd:string", + dictionary_reference_resolved=True, # no dictionary reference, on purpose + ), + ("battery", "capacityKwh"): SemanticDefinition( + kind=DataElementKind.SINGLE, + element_id="capacityKwh", + value_data_type="xsd:double", + dictionary_reference_resolved=True, # no dictionary reference, on purpose + ), +} + + +class BatteryResolver: + def resolve( + self, content_specification_ids: Sequence[str], path: SemanticPath + ) -> SemanticDefinition | None: + if PROFILE_ID not in content_specification_ids: + return None + return BATTERY_FIELDS.get(path.segments) + + +codec = DppJsonCodec() +compressed = codec.to_compressed_json(passport) +result = codec.expand_compressed_json_with_resolver(compressed, BatteryResolver()) +assert result.issues == () +assert result.passport.data_elements[0].element_id == "battery" +assert codec.to_expanded_json(result.passport) == codec.to_expanded_json(passport) +``` + +Return a complete definition for each path: + +- Set `element_id` to the field's element ID. +- Resolve every metadata item, even when the answer is "none": set + `dictionary_reference_resolved=True` (or `value_data_type_resolved=True`) for a field that + deliberately has no dictionary reference (or datatype). Otherwise the codec reports it as + missing. + +Fields the resolver does not know keep their value and appear in `result.issues`; the codec never +invents metadata. If you have the previous version of a passport, pass it as +`codec.expand_compressed_json(json_text, previous=old_passport, resolver=...)` and its metadata is +reused as well. + +`dpp_sdk.dpp4fun.Dpp4FunSemanticResolver` is the DPP4Fun implementation of the same protocol, driven +by the bundled JSON profile. Look at it if your profile grows beyond a dictionary. + +## 3. Add profile checks + +Run `DppValidator` for the standard rules, then add your own findings as `ValidationIssue`s and +combine them into one `ValidationResult`: + +```python +from dpp_sdk.core import DppValidator, ValidationIssue, ValidationResult + + +def validate_battery(dpp: DigitalProductPassport) -> ValidationResult: + issues = list(DppValidator().validate(dpp).issues) + battery = next((e for e in dpp.data_elements if e.element_id == "battery"), None) + capacity = None + if isinstance(battery, DataElementCollection): + capacity = next((e for e in battery.elements if e.element_id == "capacityKwh"), None) + if capacity is None: + issues.append( + ValidationIssue( + path="elements.battery.capacityKwh", + rule="BATTERY_CAPACITY_REQUIRED", + message="battery capacity is required", + ) + ) + return ValidationResult(issues) + + +assert validate_battery(passport).is_valid +assert not validate_battery(passport.with_updates(data_elements=())).is_valid +``` + +Use the same path style as the built-in validators (`header.`, +`elements....`) so that callers can handle all findings the same way. + +## What you get for free + +- FULL and COMPRESSED JSON in the shape the EN 18222 repository API uses, through `DppJsonCodec`. +- The HTTP clients, which take any JSON passport: `json.loads(codec.to_expanded_json(passport))`. +- Data-format and identifier checks (`DataFormatValidator`, `IdentifierValidator`); see the + [model guide](model-guide.md#validate). diff --git a/docs/model-guide.md b/docs/model-guide.md index c3d684b..fa97fd7 100644 --- a/docs/model-guide.md +++ b/docs/model-guide.md @@ -1,98 +1,370 @@ # Model guide -## Purpose and scope - -This is the normative field reference for public Python models. The [SDK usage guide](usage.md) -owns the walkthrough; the [validation guide](validation-guide.md) owns semantic-rule behavior; and -the [clients guide](../src/dpp_sdk/clients/README.md) owns HTTP payloads. All models are frozen -Pydantic models: unknown fields are rejected, `with_updates()` creates a structurally revalidated -replacement, and public field names are the emitted camelCase JSON names. - -## Construction and JSON conventions - -Required strings must be non-blank. Optional strings are either `null`/`None` or non-blank when -present; accepted values are not trimmed. UUIDs emit canonical strings, dates emit ISO dates, and -optional model fields emit `null` rather than being omitted. `OrganizationRole` is one of -`MANUFACTURER`, `SUPPLIER`, or `DISTRIBUTOR`. - -## Reusable core models - -| Model | Required fields | Optional fields and defaults | Nested values / notes | -| --- | --- | --- | --- | -| `Address` | `country: str`, `town: str` | `zipCode`, `region`, `street`: `str | None = None` | All text fields use the construction convention above. | -| `Email` | `emailAddress: str` | `typeOfEmail: str | None = None` | Semantic validation separately requires `@` in the address. | -| `Telephone` | `telephoneNumber: str` | `typeOfTelephone: str | None = None` | No telephone-format parser is supplied. | -| `Contact` | `organization: str` | `address`, `email`, `telephone`: `None` | Nested `Address`, `Email`, and `Telephone`. | -| `Organization` | `name: str` | `gln`, `productDescription`, `productDesignation`, `productFamily`, `productRoot`, `productOrderSuffix`, `uri`, `contact`, `role`: `None` | `contact: Contact | None`; `role: OrganizationRole | None`. | -| `Documentation` | none | `digitalInstructionsLink`, `safetyInstructionsLink`: `None`; `downloadable=False`; `availableForYears=None`; `paperCopyAvailableOnRequest=False` | `availableForYears` is a non-negative integer when present. | -| `Nameplate` | `gtinCode: str` | `internalArticleNumber`, `batchNumber`, `customsTariffNumber`, `uriOfTheProduct`, `manufacturer`, `supplier`: `None` | Manufacturer/supplier are `Organization | None`. | -| `PassportMetadata` | `uniqueProductIdentifier: UUID`, non-empty `passportUpdateDates: tuple[date, ...]` | `qrCodeOrDigitalTag`, `externalDocumentationLink`: `None` | Future dates are structurally accepted but semantically rejected. | -| `DppCore` | `passportMetadata`, `nameplate` | `documentation=None` | Reusable aggregate used as `Dpp4Fun.coreDpp`. | - -`Dpp` is the reusable aggregate base. It exposes read-through properties for the core values and -the identifiers `dpp_id` (the UUID as text) and `product_id` (the nameplate GTIN); applications use -a concrete aggregate such as `Dpp4Fun`. - -## DPP4Fun models - -| Model | Required fields | Optional fields and defaults | Notes | -| --- | --- | --- | --- | -| `Dimensions` | `width`, `height`, `depth`: finite non-negative `float` | `unit=None` | All three values are required at construction; semantic validation also requires a non-blank unit. | -| `ProductClassification` | `sector`, `category`: non-blank `str` | `group=None`, `subCategory=None`, `tags=()` | Tags are immutable tuples in Python and JSON arrays on the wire. | -| `Characteristics` | `productName: str` | `description`, `brand`, `productType`, `dimensions`, `weight`, `color`: `None`; `features=()` | `weight` is finite/non-negative when present; `dimensions: Dimensions | None`. | -| `Material` | `name: str` | `mandatory=False`, `portion=0.0`, `reference=None` | Portion is finite/non-negative; semantic validation adds the mandatory/positive rule. | -| `Component` | `name: str` | `reference=None` | Used in `BillOfMaterials.components`. | -| `Part` | `name: str` | `mandatory=False`, `reference=None` | Used in `BillOfMaterials.parts`. | -| `BillOfMaterials` | none | `materials=()`, `components=()`, `parts=()` | Each collection is a tuple of its typed member and serializes as `[]` when empty. | -| `Dpp4Fun` | `coreDpp: DppCore`, `classification`, `characteristics` | `billOfMaterials=None` | Furniture aggregate; inherits the `Dpp` read-through properties. | - -## Construction and serialized JSON example - -The following flat transport JSON is valid input to `from_json()` and is the shape produced by -`to_json()`. `coreDpp` is an in-memory model field; transport lifts its `passportMetadata`, -`nameplate`, and `documentation` fields to the root. +This guide shows how to build, read, edit, validate, and convert passports with `dpp_sdk.core` and +`dpp_sdk.dpp4fun`. The code blocks build on each other and run in order. + +Related pages: + +- [DPP4Fun field reference](dpp4fun-fields.md): every field name you can pass to the typed classes. +- [HTTP clients guide](clients-guide.md): sending passports to a repository and registry. +- [Building your own profile](extending.md): using Core without DPP4Fun. +- [Standards alignment and validation rules](conformance.md): clause mapping, source conflicts, + and the full rule tables. + +## Overview + +![Core passport and package relationship](architecture/python-core-model.svg) + +A `DigitalProductPassport` has one `DppHeader` and an ordered tuple of DataElements. Collections and +multi-valued elements can contain further elements. + +```text +DigitalProductPassport +├── DppHeader (required at construction) +└── data_elements: tuple[DataElement, ...] + ├── DataElementCollection → elements: tuple[DataElement, ...] + ├── SingleValuedDataElement → value: JSON value + ├── MultiValuedDataElement → value: tuple[DataElement, ...] + ├── MultiLanguageDataElement → value: tuple[MultiLanguageValue, ...] + └── RelatedResource → URL and descriptive metadata +``` + +| Element | Holds | +| --- | --- | +| `DataElementCollection` | Ordered nested elements. | +| `SingleValuedDataElement` | One JSON value (scalar, object, array, or null) and an optional `value_data_type` such as `xsd:string`. | +| `MultiValuedDataElement` | An ordered tuple of child DataElements and an optional common datatype. | +| `MultiLanguageDataElement` | Ordered `MultiLanguageValue(value=..., language=...)` entries. | +| `RelatedResource` | Link metadata: content type, URL, language, and title. | + +All models are immutable (frozen Pydantic models, tuples instead of lists). Edits return a new copy. +Python attributes are `snake_case`; JSON keys keep their camelCase names (`dpp_header.dpp_status` +becomes `"dppStatus"`). + +`dpp_sdk.dpp4fun` is a typed view over a core passport for the DPP4Fun furniture profile. You can +always get the core passport back with `as_digital_product_passport()`. + +## Build a core passport + +Use core directly when you are not working with the DPP4Fun profile: + +```python +from datetime import UTC, datetime + +from dpp_sdk.core import ( + DataElementCollection, + DigitalProductPassport, + DppHeader, + Granularity, + MultiLanguageDataElement, + MultiLanguageValue, + SingleValuedDataElement, +) + +header = DppHeader( + digital_product_passport_id="urn:example:dpp:chair-1", + unique_product_identifier="urn:example:product:chair-1", + granularity=Granularity.ITEM, + dpp_schema_version="1.0", + dpp_status="active", + last_updated=datetime(2026, 1, 1, tzinfo=UTC), + economic_operator_id="urn:example:operator:example-furniture", +) +core_passport = DigitalProductPassport( + dpp_header=header, + data_elements=( + DataElementCollection( + element_id="nameplate", + elements=( + SingleValuedDataElement( + element_id="manufacturerName", + value="Example Furniture", + value_data_type="xsd:string", + ), + MultiLanguageDataElement( + element_id="productDescription", + value=(MultiLanguageValue(value="Chair", language="en"),), + ), + ), + ), + ), +) +``` + +All header fields are optional at construction, so you can hold and inspect an incomplete passport. +Validation and JSON output need these seven: `digital_product_passport_id`, +`unique_product_identifier`, `granularity` (`MODEL`, `BATCH`, or `ITEM`), `dpp_schema_version`, +`dpp_status`, `last_updated` (timezone-aware), and `economic_operator_id`. `facility_id` and +`content_specification_ids` are optional. + +## Build a DPP4Fun passport + +`Dpp4Fun.create()` takes a header plus any number of aspects and at most one product type +(Characteristics variant). Each aspect class has a `create(**fields)` method: + +```python +from dpp_sdk.dpp4fun import ( + BillOfMaterials, + ChairCharacteristics, + Dpp4Fun, + Dpp4FunQuantity, + LengthUnit, + Material, + Nameplate, +) + +passport = Dpp4Fun.create( + header, + Nameplate.create(manufacturerName="Example Furniture", gtinCode="04012345678901"), + BillOfMaterials.create( + materialSet=( + Material.create(name="Oak", portion=0.75).element, + Material.create(name="Steel", portion=0.25).element, + ) + ), + ChairCharacteristics.create(color="blue").with_quantity( + "height", Dpp4FunQuantity(value=80.0, unit=LengthUnit.CENTIMETER) + ), +) +``` + +Things to know: + +- **Field names** are the camelCase element IDs from the profile (`manufacturerName`, not + `manufacturer_name`). The [field reference](dpp4fun-fields.md) lists all of them per aspect. An + unknown name raises `KeyError`. +- **Repeated items** (paths with `[*]` in the field reference) are built with a composite class such + as `Material`, `Component`, `Certificate`, or `RepairRecord`. Pass their `.element` values as a + tuple. The SDK assigns item IDs (`material_00`, `material_01`, …) in input order. +- **Quantities** pair a number with a unit. `Dpp4FunQuantity` accepts `LengthUnit`, `MassUnit`, or + `VolumeUnit`; a unit of the wrong dimension is rejected. `with_quantity()` also fills the matching + unit field (`heightUnit` above). +- **Unknown elements** that fit a core DataElement shape are kept, not dropped, when you read or + edit a passport. + +## Read fields + +```python +nameplate = passport.typed_aspect(Nameplate) # None if the passport has no nameplate +assert nameplate is not None +assert nameplate.field("manufacturerName").value == "Example Furniture" + +chair = passport.selected_characteristics() # the product type, or None +assert isinstance(chair, ChairCharacteristics) +height = chair.quantity("height") +assert height is not None and height.value == 80.0 and height.unit is LengthUnit.CENTIMETER + +bill = passport.typed_aspect(BillOfMaterials) +assert bill is not None +materials = bill.field("materialSet").value # tuple of DataElementCollections +assert [Material.from_element(item).field("name").value for item in materials] == ["Oak", "Steel"] + +assert passport.header.digital_product_passport_id == "urn:example:dpp:chair-1" +``` + +- `typed_aspect(AspectClass)` returns the typed aspect or `None`. +- `field(name)` returns the core DataElement (read `.value`) or `None` when the field is not set. +- `quantity(name)` returns a `Dpp4FunQuantity` or `None`. +- `get_aspect("nameplate")` returns the raw `DataElementCollection` by element ID. +- `Dpp4Fun.from_passport(core_passport)` wraps any core passport, for example one you decoded with + the core codec. + +## Edit + +Every edit returns a new object; the original stays unchanged. + +```python +renamed = nameplate.with_field("manufacturerName", "Example Furniture GmbH") +updated = passport.with_updates(aspects=(renamed,)) # replaces the nameplate, keeps the rest +assert updated.typed_aspect(Nameplate).field("manufacturerName").value == "Example Furniture GmbH" +assert passport.typed_aspect(Nameplate).field("manufacturerName").value == "Example Furniture" + +retired = core_passport.with_updates( + dpp_header=core_passport.dpp_header.with_updates(dpp_status="inactive") +) +assert retired.dpp_header.dpp_status == "inactive" +``` + +`with_updates()` and `with_field()` check structure (types, element shapes, profile bindings) but +do not run the semantic rules. Validate explicitly before you send a passport anywhere. + +## Validate + +Validation is a separate step. It never changes the passport and never raises for findings; it +returns an ordered report. + +```python +from dpp_sdk import validate_dpp4fun +from dpp_sdk.core import DppValidator, ValidationStatus + +report = validate_dpp4fun(updated) +assert report.is_valid + +empty = DppValidator().validate(DigitalProductPassport(dpp_header=DppHeader())) +assert empty.status is ValidationStatus.INVALID +first = empty.issues[0] +assert first.path == "header.digitalProductPassportId" +assert first.rule == "EN18223_HEADER_DPP_ID_REQUIRED" +``` + +| Validator | Checks | +| --- | --- | +| `DppValidator` | Required header fields, element IDs and cardinality, kind/datatype consistency, nested structure. | +| `DataFormatValidator` | JSON value types, language and URI syntax, list structure. | +| `IdentifierValidator` | Identifier syntax for an identifier method you choose (GS1 Digital Link, GS1 GLN, Identification Link). Only local syntax: no issuance, uniqueness, or resolution. | +| `validate_dpp4fun()` | All of the above (identifiers only when you pass an `IdentifierValidationContext`), then the DPP4Fun D4.1 profile rules. | + +Each `ValidationIssue` has `path`, `rule`, `message`, and `severity` (`ERROR` or `WARNING`). +`ValidationResult.status` is `VALID` when there are no errors, `UNSUPPORTED` when the only errors +come from unsupported identifier methods, and `INVALID` otherwise; `is_valid` reflects it. Paths +look like `header.`, `elements..`, or +`elements..[index]` for repeated items. + +To check identifiers too, say which method each identifier uses; the SDK never guesses it: + +```python +from dpp_sdk.core import ( + EconomicOperatorIdentifierMethod, + IdentifierValidationContext, + ProductIdentifierMethod, +) + +context = IdentifierValidationContext( + product_identifier_method=ProductIdentifierMethod.GS1_DIGITAL_LINK, + economic_operator_identifier_method=EconomicOperatorIdentifierMethod.GS1_GLN, +) +identifier_report = validate_dpp4fun(updated, context) +assert not identifier_report.is_valid # the urn:example identifiers are not GS1 identifiers +``` + +The full rule list, with rule codes and the source each rule comes from, is in +[conformance.md](conformance.md#core-rules). + +## Convert to and from JSON + +There are two JSON forms: + +- **FULL** (also called expanded) carries every element's `objectType`, datatype, and dictionary + reference. It can always be read back without extra information. +- **COMPRESSED** keeps only values, keyed by element ID. It is shorter, but reading it back needs + the metadata from somewhere: the DPP4Fun profile, a previous version of the passport, or your own + [semantic resolver](extending.md). + +The core passport above in FULL form (header fields sit at the root next to `elements`): ```json { - "passportMetadata": { - "uniqueProductIdentifier": "11111111-1111-1111-1111-111111111111", - "passportUpdateDates": ["2026-06-29"], - "qrCodeOrDigitalTag": null, - "externalDocumentationLink": null - }, + "digitalProductPassportId": "urn:example:dpp:chair-1", + "uniqueProductIdentifier": "urn:example:product:chair-1", + "granularity": "item", + "dppSchemaVersion": "1.0", + "dppStatus": "active", + "lastUpdated": "2026-01-01T00:00:00Z", + "economicOperatorId": "urn:example:operator:example-furniture", + "contentSpecificationIds": [], + "elements": [ + { + "elementId": "nameplate", + "objectType": "DataElementCollection", + "elements": [ + { + "elementId": "manufacturerName", + "objectType": "SingleValuedDataElement", + "value": "Example Furniture", + "valueDataType": "xsd:string" + }, + { + "elementId": "productDescription", + "objectType": "MultiLanguageDataElement", + "value": [{"value": "Chair", "language": "en"}] + } + ] + } + ] +} +``` + +The same passport in COMPRESSED form: + +```json +{ + "digitalProductPassportId": "urn:example:dpp:chair-1", + "uniqueProductIdentifier": "urn:example:product:chair-1", + "granularity": "item", + "dppSchemaVersion": "1.0", + "dppStatus": "active", + "lastUpdated": "2026-01-01T00:00:00Z", + "economicOperatorId": "urn:example:operator:example-furniture", + "contentSpecificationIds": [], "nameplate": { - "gtinCode": "04012345678901", - "internalArticleNumber": null, - "batchNumber": null, - "customsTariffNumber": null, - "uriOfTheProduct": null, - "manufacturer": {"name": "Cir4Fun Furniture GmbH", "role": "MANUFACTURER"}, - "supplier": null - }, - "documentation": null, - "classification": {"sector": "Furniture", "group": null, "category": "Seating", "subCategory": null, "tags": []}, - "characteristics": {"productName": "ErgoChair", "description": null, "brand": null, "productType": null, "dimensions": null, "weight": null, "color": null, "features": []}, - "billOfMaterials": {"materials": [], "components": [], "parts": []} + "manufacturerName": "Example Furniture", + "productDescription": [{"value": "Chair", "language": "en"}] + } } ``` -## Immutable updates and validation boundary +Use `Dpp4FunJsonCodec` for DPP4Fun passports and `DppJsonCodec` for core passports. The two codecs +name some methods differently: + +| Task | `Dpp4FunJsonCodec` | `DppJsonCodec` (core) | +| --- | --- | --- | +| Write FULL | `to_json()` or `to_expanded_json()` | `to_expanded_json()` | +| Read FULL | `from_json()` or `from_expanded_json()` | `from_expanded_json()` | +| Read FULL and validate | `from_json_and_validate()` | — | +| Write COMPRESSED | `to_compressed_json()` | `to_compressed_json()` | +| Read COMPRESSED | `from_compressed_json()`, `from_compressed_json_and_validate()` | `expand_compressed_json(json, previous=None, resolver=None)` | +| Read COMPRESSED with a report of unresolved fields | `expand_compressed_json()` | `expand_compressed_json()` | -Use `with_updates()` on the value being replaced; it re-enters structural Pydantic validation and -does not mutate the original value. It does not automatically perform semantic validation, so call -`validate_dpp_core()` or `validate_dpp4fun()` when that boundary matters. +`dpp_sdk` also exports `to_json()`, `from_json()`, and `from_json_and_validate()` as shortcuts for +the DPP4Fun codec's FULL methods. The codecs work with JSON **strings**; the HTTP clients send and +return Python values, so use `json.loads()` / `json.dumps()` between them (see the +[clients guide](clients-guide.md#decode-a-response-into-a-typed-passport)). ```python -updated = dpp.with_updates( - characteristics=dpp.characteristics.with_updates(productName="ErgoChair Pro") -) -validate_dpp4fun(updated) -assert dpp.characteristics.productName == "ErgoChair" +import json + +from dpp_sdk import Dpp4FunJsonCodec +from dpp_sdk.core import DppJsonCodec + +codec = Dpp4FunJsonCodec() +full_json = codec.to_json(updated) +assert codec.from_json(full_json) == updated + +compressed_json = codec.to_compressed_json(updated) +assert codec.from_compressed_json(compressed_json) == updated # the profile supplies metadata + +core_full = DppJsonCodec().to_expanded_json(core_passport) +assert json.loads(core_full)["elements"][0]["elementId"] == "nameplate" +``` + +When expanding COMPRESSED JSON, the codec takes type information only from the profile, a previous +passport, or a resolver. A field it cannot resolve keeps its value and is listed in the result's +`issues`; the SDK never invents a datatype or dictionary reference. + +### Errors + +| Exception | When | +| --- | --- | +| `DppJsonException` (a `DppMappingError`) | Malformed JSON, `null` or non-object root, `NaN`/`Infinity`, a structure that does not map to the model, or writing a passport whose header lacks one of the seven required fields. The parser error is kept as `__cause__`. | +| `DppValidationError` | Only from the `..._and_validate()` methods: the JSON was read, but validation found errors. | + +```python +from dpp_sdk import DppMappingError, DppValidationError, from_json_and_validate + +for raw_json in (full_json, "{not json"): + try: + incoming = from_json_and_validate(raw_json) + except DppMappingError as exc: + print(f"Could not read passport JSON: {exc}") + except DppValidationError as exc: + print(f"Passport has validation errors: {exc}") ``` -## Limitations and next steps +## Text and number conventions -This guide does not define generic models for every product sector, service persistence, or an -implicit validation-on-parse mode. See the [validation guide](validation-guide.md) for error stages, -the [validation-rule reference](validation-rules.md) for exact semantic rules, and the -[DPP4Fun module guide](../src/dpp_sdk/dpp4fun/README.md) for the codec workflow. +- Values are never trimmed, normalized, sorted, or deduplicated on construction or validation. +- A text counts as blank when it contains only Unicode White_Space characters. U+200B (zero-width + space) is not white space, so it counts as visible text. +- Duplicate checks compare text with normalized whitespace and case-insensitively. +- Non-finite numbers (`NaN`, `Infinity`) are rejected when a model is built and when JSON is read + or written. diff --git a/docs/overview.md b/docs/overview.md deleted file mode 100644 index c0ba275..0000000 --- a/docs/overview.md +++ /dev/null @@ -1,44 +0,0 @@ -# SDK overview - -## Purpose and scope - -`dpp-sdk` is a reusable Python client library for DPP models, semantic validation, DPP4Fun JSON, -and synchronous repository and registry clients. It does not implement services, persistence, -Docker, Spring, or internal Java routes. - -## Architecture and package boundaries - -![Python SDK package boundaries](architecture/python-sdk-overview.svg) - -`dpp_sdk.core` owns reusable immutable models, structural constraints, and core semantic -validation. `dpp_sdk.dpp4fun` builds on core and owns the furniture aggregate, its semantic -validation, and flat/nested JSON mapping. `dpp_sdk.clients` stays model-independent and performs -HTTP calls with a caller-supplied codec and validator. The Mock-services demo is optional and uses -the installed SDK against Java repository and registry images. - -## Supported concepts - -The SDK supports model construction, explicit semantic validation, JSON serialization and mapping, -immutable replacements through `with_updates()`, repository CRUD/read operations, fine-grained -element operations, and registry registration. The clients call public service APIs only; they do -not run, configure, or persist those services. - -## Intentional exclusions and limitations - -This repository does not provide a repository or registry server, persistence, lifecycle storage, -Docker or Spring runtime behavior, PostgreSQL integration, Postman/Swagger artifacts, internal Java -routes, authentication, retries, caching, pagination, or registry read-back/cleanup operations. -The Mock-services demo is optional compatibility evidence and is not an SDK dependency. - -## Prerequisites - -Use Python 3.11 or newer. Docker and Compose are required only for the separate -[Mock-services demo](../examples/mock-services-demo/README.md). - -## Reading order and next steps - -Start with [SDK usage](usage.md). Use the [model guide](model-guide.md) for fields and JSON/null -semantics, the [validation guide](validation-guide.md) and [validation-rule reference] -(validation-rules.md) for validation/codec behavior, and the [clients guide] -(../src/dpp_sdk/clients/README.md) for request/response payloads and errors. Use -[RELEASING.md](../RELEASING.md) for development and release validation. diff --git a/docs/usage.md b/docs/usage.md deleted file mode 100644 index 4d5404f..0000000 --- a/docs/usage.md +++ /dev/null @@ -1,212 +0,0 @@ -# SDK usage - -## Purpose and scope - -This is the end-to-end consumer walkthrough. Use the [model guide](model-guide.md) for field and -null semantics and the [validation guide](validation-guide.md) for the semantic-rule boundary. - -## Installation - -**Purpose:** install the published SDK before following this walkthrough. **Run from:** any -directory; this is directory-independent because it installs a named published distribution. -**Prerequisites:** Python 3.11 or newer on `PATH` and access to the selected package index. - -### PowerShell - -PowerShell: - -```powershell -python -m pip install dpp-sdk -``` - -### Linux/macOS - -```bash -python -m pip install dpp-sdk -``` - -For an exact published version, a compatible release line, or this local checkout, use the -[root installation guide](../README.md#install). It explains when a published pip install is the -right choice and when local source installation is appropriate. For a local checkout, use the -repository's development environment and run its configured checks from the repository root -through the [release guide](../RELEASING.md). **Expected result:** an installed `dpp_sdk` import. -**Cleanup:** none. The -[Mock-services demo](../examples/mock-services-demo/README.md) has separate installation -instructions because it is a separate, unpublished consumer package. - -## Imports - -The top-level package exports the public models, semantic validators, common errors, and DPP4Fun -codec helpers. Clients and client request/response types are exported from `dpp_sdk.clients`. - -```python -from dpp_sdk import Dpp4FunJsonCodec, from_json, to_json, validate_dpp4fun -from dpp_sdk.clients import DppRegistryClient, DppRepoClient, RegisterDppRequest -``` - -## Model construction - -Create models with their documented camelCase transport fields. Pydantic construction checks the -structural contract immediately; the following complete example is also the basis of the test -fixtures. - -```python -from datetime import date -from uuid import UUID - -from dpp_sdk import ( - Address, - BillOfMaterials, - Characteristics, - Contact, - Dimensions, - Documentation, - Dpp4Fun, - DppCore, - Email, - Material, - Nameplate, - Organization, - OrganizationRole, - PassportMetadata, - ProductClassification, -) - -manufacturer = Organization( - name="ACME Furniture GmbH", - gln="4012345000009", - role=OrganizationRole.MANUFACTURER, - contact=Contact( - organization="ACME HQ", - address=Address(country="DE", town="Berlin", street="Hauptstr. 1", zipCode="10115"), - email=Email(emailAddress="info@acme.example", typeOfEmail="business"), - ), -) -dpp = Dpp4Fun( - coreDpp=DppCore( - passportMetadata=PassportMetadata( - uniqueProductIdentifier=UUID("11111111-1111-1111-1111-111111111111"), - passportUpdateDates=[date(2024, 1, 1)], - ), - nameplate=Nameplate(gtinCode="GTIN-0001", manufacturer=manufacturer), - documentation=Documentation( - digitalInstructionsLink="https://acme.example/docs", - downloadable=True, - ), - ), - classification=ProductClassification(sector="Furniture", category="Office Chair"), - characteristics=Characteristics( - productName="ErgoChair Pro", - dimensions=Dimensions(width=60.0, height=120.0, depth=60.0, unit="cm"), - ), - billOfMaterials=BillOfMaterials(materials=[Material(name="steel", portion=0.6)]), -) -``` - -`dpp.dpp_id` is the string form of `uniqueProductIdentifier`; `dpp.product_id` is `gtinCode`. - -## Validation - -Construction is structural. Call the semantic validator explicitly before using a DPP as a valid -domain object or creating it in a repository: - -```python -from dpp_sdk import DppValidationError, validate_dpp4fun - -try: - validate_dpp4fun(dpp) -except DppValidationError as exc: - print(f"DPP semantic validation failed: {exc}") -``` - -The validators are fail-fast and do not mutate or repair the model. The -[core module guide](../src/dpp_sdk/core/README.md) and -[DPP4Fun module guide](../src/dpp_sdk/dpp4fun/README.md) own the detailed rule descriptions. - -## Serialization and deserialization - -Use the DPP4Fun helpers when your application exchanges DPP4Fun JSON: - -```python -raw_json = to_json(dpp) -decoded = from_json(raw_json) -validate_dpp4fun(decoded) -assert to_json(decoded) == raw_json -``` - -`from_json()` maps a supported flat or nested payload but does not itself apply semantic -validation. `from_json_and_validate()` is available when an application needs both steps together. - -## Immutable updates - -Public domain models are frozen. Use `with_updates()` to construct a new, structurally revalidated -instance, then run semantic validation when the change can affect semantic or cross-model rules. - -```python -updated = dpp.with_updates( - characteristics=dpp.characteristics.with_updates(productName="ErgoChair Pro 2") -) -validate_dpp4fun(updated) -assert dpp.characteristics.productName == "ErgoChair Pro" -``` - -Do not use unchecked `model_copy(update=...)` as a public update mechanism: it can bypass the -structural construction boundary. - -## Repository client - -The repository client needs a base URL, a codec, and a validator. Context management closes the -internally created HTTPX client: - -```python -from dpp_sdk import Dpp4FunJsonCodec, validate_dpp4fun -from dpp_sdk.clients import DppRepoClient - -with DppRepoClient( - "https://repo.example.com", - codec=Dpp4FunJsonCodec(), - validator=validate_dpp4fun, -) as repo: - created = repo.create_dpp(dpp) - fetched = repo.read_dpp_by_id(created.dppId) -``` - -See the [clients module guide](../src/dpp_sdk/clients/README.md) for all supported repository -operations and their error behavior. - -## Registry client - -Register only after the DPP is available at the repository endpoint provided in the request: - -```python -from dpp_sdk.clients import DppRegistryClient, RegisterDppRequest - -with DppRegistryClient("https://registry.example.com") as registry: - registration = registry.post_new_dpp_to_registry( - RegisterDppRequest( - uniqueProductIdentifier=dpp.product_id, - digitalProductPassportId=dpp.dpp_id, - uniqueEconomicOperatorIdentifier="operator-123", - dppApiEndpoint="https://repo.example.com", - ) - ) -``` - -The public registry client currently exposes health checking and registration, not registry -read-back or cleanup operations. - -## Errors - -Catch `DppValidationError` and `DppMappingError` for model/codec work. Client code can catch the -more specific classes exported by `dpp_sdk.clients`, or their common `DppClientError` base. Avoid -collapsing validation, mapping, network, HTTP, and API-envelope failures into one application error -when callers need to decide whether a request was sent. - -## Resource ownership - -Each client closes an HTTPX client that it created itself when `close()` is called or its context -manager exits. If you inject an `httpx.Client`, it stays caller-owned and is not closed by the SDK. - -Next: consult the [clients module guide](../src/dpp_sdk/clients/README.md) for network behavior, the -[core module guide](../src/dpp_sdk/core/README.md) for reusable-model rules, or the -[DPP4Fun module guide](../src/dpp_sdk/dpp4fun/README.md) for aggregate and codec rules. diff --git a/docs/validation-guide.md b/docs/validation-guide.md deleted file mode 100644 index 879c16e..0000000 --- a/docs/validation-guide.md +++ /dev/null @@ -1,59 +0,0 @@ -# Validation guide - -## Purpose and scope - -Construction, semantic validation, and JSON mapping are deliberately separate. Use this guide to -choose the boundary; use the [validation-rule reference](validation-rules.md) for individual rules -and the [model guide](model-guide.md) for fields/defaults/null behavior. - -## Construction versus semantic validation - -Pydantic construction rejects missing required fields, blank contracted strings, unknown fields, -non-finite or negative constrained numeric values, and invalid nested model shape. It produces -frozen immutable values but does not run every business rule. `validate_dpp_core()` and -`validate_dpp4fun()` are explicit, non-mutating, fail-fast calls: they raise `DppValidationError` -for the first semantic violation and preserve the input object. - -The aggregate order is fixed: `validate_dpp4fun()` validates the core, product classification, -characteristics, Bill of Materials, then cross-object rules. Collection failures name the indexed -member, for example `BillOfMaterials.materials[0]` or `ProductClassification.tags[1]`. - -## Codec input, output, and failures - -`to_json(dpp)` serializes a `Dpp4Fun` aggregate to a flat JSON object and rejects non-finite output -through `DppMappingError` with the underlying `ValueError` as its cause. `from_json(raw)` accepts a -flat object or a nested `coreDpp` object, normalizes to the nested in-memory model, and does not run -semantic validation. When both shapes carry core fields, the nested `coreDpp` values win. - -`from_json()` rejects malformed JSON, `null`, non-object roots, non-finite JSON constants, null -string-list members, and Pydantic mapping failures with `DppMappingError`; parser and Pydantic -causes are retained. `from_json_and_validate(raw)` maps first and then invokes semantic validation. -Empty tuple-backed collections serialize as `[]`; present optional model fields serialize as `null`. - -```python -from dpp_sdk import DppMappingError, DppValidationError, from_json_and_validate, to_json - -try: - passport = from_json_and_validate(raw_json) - outgoing_json = to_json(passport) -except DppMappingError: - # JSON syntax, root, normalization, or structural mapping failure. - raise -except DppValidationError: - # Semantically invalid, but structurally representable, passport. - raise -``` - -## Immutable updates - -`with_updates()` structurally revalidates the changed model and returns a new frozen value. It does -not call semantic validators implicitly. Perform explicit validation after an update when the -application requires a semantically valid aggregate. - -## Errors, limitations, and next steps - -Validators report the first observed failure only; they do not collect all problems, trim/repair -text, sort/deduplicate collections, or turn mapping errors into validation errors. The transport -codec is specific to DPP4Fun and does not create a generic core JSON codec. See the -[validation-rule reference](validation-rules.md), [model guide](model-guide.md), and -[SDK usage](usage.md) for the consumer workflow. diff --git a/docs/validation-rules.md b/docs/validation-rules.md deleted file mode 100644 index 9806890..0000000 --- a/docs/validation-rules.md +++ /dev/null @@ -1,50 +0,0 @@ -# Validation-rule reference - -## Purpose and scope - -This is the semantic-rule lookup for explicitly invoked validators. Construction constraints and -field defaults belong to the [model guide](model-guide.md). Each public validator is fail-fast and -raises `DppValidationError`; `None` is accepted by leaf validators unless the row says it is -required by that validator. - -## Core rules - -| Validator | Rule and null behavior | Error path / evidence | -| --- | --- | --- | -| `validate_address` | `None` accepted; country/town and present optional text are non-blank. | `Address.*`; `src/dpp_sdk/core/validation.py`; `test_core_validation.py`. | -| `validate_email` | `None` accepted; address is non-blank and contains `@`; present type is non-blank. | `Email.emailAddress`; `test_email_must_contain_at`. | -| `validate_telephone` | `None` accepted; number and present type are non-blank. | `Telephone.*`; core validation tests. | -| `validate_contact` | `None` accepted; organization is non-blank and at least one address/email/telephone channel exists. | `Contact.*`; `test_contact_requires_a_channel`. | -| `validate_organization` | `None` is an error; name and present URI are non-blank; validates a present contact. | `Organization.*`; `test_organization_is_required`. | -| `validate_nameplate` | `None` is an error; GTIN and present identifiers are non-blank; manufacturer or supplier is required and each supplied slot must have its matching role. | `Nameplate.*`; `test_nameplate_requires_manufacturer_or_supplier`, role tests. | -| `validate_documentation` | `None` accepted; present links are non-blank; `downloadable=True` or present `availableForYears` requires a link. | `Documentation.*`; `test_documentation_downloadable_requires_link`. | -| `validate_passport_metadata` | `None` is an error; identifier and at least one date required; no null or future date; present tag/link text non-blank. | `PassportMetadata.passportUpdateDates[index]`; `test_future_date_rejected`. | -| `validate_dpp_core` | `None` is an error; order is metadata, nameplate, documentation. | `DppCore`; `test_core_aggregate_order_metadata_then_nameplate_then_documentation`. | - -## DPP4Fun rules - -| Validator | Rule and null behavior | Error path / evidence | -| --- | --- | --- | -| `validate_dimensions` | `None` accepted; supplied dimensions must be finite/non-negative; an existing object requires non-blank unit. | `Dimensions.*`; `test_dimensions_require_unit_when_values_present`. | -| `validate_material` | `None` accepted; name/reference non-blank; portion finite/non-negative; mandatory material requires portion greater than zero. | `Material.*`; `test_material_mandatory_requires_positive_portion`. | -| `validate_component` / `validate_part` | `None` accepted; name and present reference non-blank. | `Component.*` / `Part.*`; Bill-of-Materials tests. | -| `validate_bill_of_materials` | `None` accepted; null members fail with indexed paths; every member is validated before normalized name/reference duplicate detection. | `BillOfMaterials.materials[index]`, `components[index]`, `parts[index]`; duplicate/null-member tests. | -| `validate_product_classification` | `None` is an error; sector/category and present group/subcategory are non-blank; subcategory needs category and group needs sector; tags contain no null, blank, or normalized duplicate member. | `ProductClassification.*`, `tags[index]`; `test_tags_reject_duplicates`. | -| `validate_characteristics` | `None` is an error; product name non-blank; weight finite/non-negative; validates dimensions; features contain no null, blank, or normalized duplicate member. | `Characteristics.*`, `features[index]`; `test_features_reject_blank`. | -| `validate_dpp4fun` | `None` is an error; order is core, classification, characteristics, BOM, then cross rules. | `Dpp4Fun`; `test_dec_005_supported_aggregate_validation_order`. | -| DPP4Fun cross rules | Category and product type must contain one another when both have text; metadata external-documentation link requires a `Documentation` object. | Cross-object error text; `test_cross_rule_category_producttype_inconsistent`, `test_cross_rule_external_link_requires_documentation`. | - -## Text, numeric, and collection conventions - -The frozen Unicode whitespace table determines blankness; U+200B is visible text rather than -blank. Validation does not normalize input before storing it, but duplicate comparisons use the -contract whitespace normalization and case-insensitive comparison. Non-finite numeric values are -rejected at construction where constrained and are also defensively rejected by semantic validators -when bypassed values are supplied. - -## Codec boundary, limitations, and next steps - -JSON mapping is not a semantic validator: mapping failures are `DppMappingError`, while these rules -raise `DppValidationError`. See the [validation guide](validation-guide.md) for flat/nested codec -forms, root/null behavior, cause chaining, and round trips; see the [model guide](model-guide.md) -for construction fields and defaults. diff --git a/examples/consumer_lifecycle.py b/examples/consumer_lifecycle.py new file mode 100644 index 0000000..fd86b16 --- /dev/null +++ b/examples/consumer_lifecycle.py @@ -0,0 +1,134 @@ +"""Full passport lifecycle against a DPP repository and registry. + +Creates a test passport, validates it, saves it, reads it back, updates it, reads the +previous version, registers it, and finally deletes it from the repository. + +Run it (services must be running, see the README): + + python examples/consumer_lifecycle.py + +Set DPP_REPO_BASE_URL and DPP_REGISTRY_BASE_URL to use other services. The registry +record is not removed (the registry API has no delete), so use disposable services. +""" + +import json +from datetime import UTC, datetime +from uuid import uuid4 + +from dpp_sdk import Dpp4FunJsonCodec, DppHeader, Granularity, validate_dpp4fun +from dpp_sdk.clients import ( + DppRegistryClient, + DppRepoClient, + DppRepresentation, + RegisterDppRequest, +) +from dpp_sdk.dpp4fun import ChairCharacteristics, Dpp4Fun, Nameplate + + +def run_consumer_workflow(repository_url: str, registry_url: str) -> dict[str, object]: + token = uuid4().hex + passport = Dpp4Fun.create( + DppHeader( + digital_product_passport_id=f"urn:example:dpp:{token}", + unique_product_identifier=f"urn:example:product:{token}", + granularity=Granularity.ITEM, + last_updated=datetime.now(UTC), + dpp_status="active", + economic_operator_id="urn:example:operator:demo", + ), + Nameplate.create(manufacturerName="Example Furniture"), + ChairCharacteristics.create(color="blue"), + ) + + report = validate_dpp4fun(passport) + if not report.is_valid: + raise ValueError(report.issues) + + codec = Dpp4FunJsonCodec() + full_json = codec.to_json(passport) + compressed_json = codec.to_compressed_json(passport) + passport_id = passport.header.digital_product_passport_id + if passport_id is None: + raise ValueError("the test passport requires a DPP identifier") + + repository_created = False + result: dict[str, object] = {} + with DppRepoClient(repository_url) as repository, DppRegistryClient(registry_url) as registry: + try: + created = repository.create_dpp(json.loads(full_json), DppRepresentation.FULL) + repository_created = True + passport_id = created.digital_product_passport_id or passport_id + + full_response = repository.read_dpp_by_id(passport_id, DppRepresentation.FULL) + stored = codec.from_json_and_validate(json.dumps(full_response.payload)) + history_at = datetime.now(UTC) + + nameplate = stored.typed_aspect(Nameplate) + if nameplate is None: + raise ValueError("the stored passport has no nameplate aspect") + updated = stored.with_updates( + header=stored.header.with_updates(last_updated=datetime.now(UTC)), + aspects=(nameplate.with_field("manufacturerName", "Example Furniture Ltd."),), + ) + updated_report = validate_dpp4fun(updated) + if not updated_report.is_valid: + raise ValueError(updated_report.issues) + + updated_full = json.loads(codec.to_json(updated)) + repository.update_dpp_by_id( + passport_id, + {"elements": updated_full["elements"]}, + DppRepresentation.FULL, + ) + + verified_response = repository.read_dpp_by_id(passport_id, DppRepresentation.FULL) + verified = codec.from_json_and_validate(json.dumps(verified_response.payload)) + + history_response = repository.read_dpp_version_by_id_and_date( + passport_id, history_at, DppRepresentation.FULL + ) + historical = codec.from_json_and_validate(json.dumps(history_response.payload)) + historical_nameplate = historical.typed_aspect(Nameplate) + if ( + historical_nameplate is None + or historical_nameplate.field("manufacturerName").value != "Example Furniture" + ): + raise ValueError("the historical read did not return the original version") + + registration = registry.post_new_dpp_to_registry( + RegisterDppRequest( + unique_product_identifier=verified.header.unique_product_identifier, + digital_product_passport_id=passport_id, + unique_economic_operator_identifier=verified.header.economic_operator_id, + dpp_api_endpoint=f"{repository_url}/v1/dpps", + ) + ) + result |= { + "passport_id": passport_id, + "full_json": full_json, + "compressed_json": compressed_json, + "updated_manufacturer": verified.typed_aspect(Nameplate) + .field("manufacturerName") + .value, + "historical_passport": historical, + "registration_id": registration.registration_id, + } + finally: + if repository_created: + deleted = repository.delete_dpp_by_id(passport_id) + result["repository_cleanup_status"] = deleted.status_code + + return result + + +if __name__ == "__main__": + import os + + outcome = run_consumer_workflow( + os.environ.get("DPP_REPO_BASE_URL", "http://localhost:8080"), + os.environ.get("DPP_REGISTRY_BASE_URL", "http://localhost:8081"), + ) + print("Passport:", outcome["passport_id"]) + print("Updated manufacturer:", outcome["updated_manufacturer"]) + print("Registration:", outcome["registration_id"]) + print("Repository cleanup:", outcome["repository_cleanup_status"]) diff --git a/examples/mock-services-demo/.env.example b/examples/mock-services-demo/.env.example index d8c916b..930f355 100644 --- a/examples/mock-services-demo/.env.example +++ b/examples/mock-services-demo/.env.example @@ -1,8 +1,8 @@ # Copy this file to .env before starting Docker. It is intentionally non-secret and ignored by Git. # Use a unique project name and unused paired ports when another Compose project is already running. COMPOSE_PROJECT_NAME=dpp-mock-services-demo-local -DPP_REPO_IMAGE=ghcr.io/cir4fun-eu/dpp-repo-api:0.5.1 -DPP_REGISTRY_IMAGE=ghcr.io/cir4fun-eu/dpp-registry-api:0.5.1 +DPP_REPO_IMAGE=ghcr.io/cir4fun-eu/dpp-repo-api:0.6.0 +DPP_REGISTRY_IMAGE=ghcr.io/cir4fun-eu/dpp-registry-api:0.6.0 DPP_REPO_BASE_URL=http://localhost:18080 DPP_REGISTRY_BASE_URL=http://localhost:18081 MOCK_REPO_PORT=18080 diff --git a/examples/mock-services-demo/ADVANCED_OPERATIONS.md b/examples/mock-services-demo/ADVANCED_OPERATIONS.md index 51ce01d..609d85e 100644 --- a/examples/mock-services-demo/ADVANCED_OPERATIONS.md +++ b/examples/mock-services-demo/ADVANCED_OPERATIONS.md @@ -1,66 +1,93 @@ # Advanced mock-services operations and troubleshooting -The [demo README](README.md) is the normal consumer path: install, offline -`sdk`, connected `demo`, full test suites, and project-scoped cleanup. This is -an optional reference for extra lifecycle commands, alternate profiles, -troubleshooting, maintainer verification, and CI/release evidence. - -**Run from:** the repository root—the directory containing the root `pyproject.toml`. - -## Profiles and evidence - -| Profile | Use | Status | -|---|---|---| -| `.env` | Your ignored local project name, ports, and current `0.5.1` images | Default after copying `.env.example` | -| `0.5.1.env` | Current default semantic-version images | Maintained | -| `pinned.env` | Previous reproducible tag-plus-digest evidence | Historical | -| `0.5.0.env` | Previous semantic-version evidence | Historical | -| `0.4.0.env` | Legacy compatibility evidence; requires `--legacy` | Informational | - -Copy `.env.example` to `.env` first, then set `COMPOSE_PROJECT_NAME` and paired host ports/base -URLs there. The lifecycle script uses `.env` by default; select a committed profile explicitly with -`$envFile` or `env_file` when needed. A missing profile is a configuration failure; it is not silently -replaced. - -`demo` is the small live educational journey. Maintainers use -`python -m dpp_mock_services_demo.maintainer sdk-contracts`, `live`, and `verify` for the exhaustive -SDK, service, package, image, and policy evidence. `full`, `verify`, `integration`, `services`, and -`all` remain compatibility commands. Pass the startup project with `--compose-project` so strict -image inspection targets the same isolated stack. Passing an educational step is evidence for that -step, not a compatibility certification. +The [demo README](README.md) covers the normal path. This page has extra commands for ports, +other service versions, diagnosis, cleanup, and maintainer verification. + +**Run from:** the repository root (the folder that contains the root `pyproject.toml`), with the +development environment activated. + +## Set up the variables first + +The commands on this page use the variables below. Run this block once in each new terminal +before any other block. + +PowerShell: + +```powershell +$demoDir = (Resolve-Path .\examples\mock-services-demo).Path +$envFile = Join-Path $demoDir ".env" +$composeFile = Join-Path $demoDir "compose.yaml" +$project = "dpp-mock-services-demo-local" +``` + +Bash (Linux/macOS): + +```bash +demo_dir="$(cd ./examples/mock-services-demo && pwd)" +env_file="$demo_dir/.env" +compose_file="$demo_dir/compose.yaml" +project="dpp-mock-services-demo-local" +``` ## Alternate ports -Change each container port together with its public base URL in `.env` before `config`, `pull`, or `up`. +The demo's `.env` (created from `.env.example`) uses port 18080 for the repository and 18081 for the +registry. Without an `.env` file, as in the README quickstart, `compose.yaml` falls back to 8080 and +8081. If your ports are taken, set all six values together, then start the services again. You can +also put the same values into `.env`. PowerShell: ```powershell -$env:DPP_REPO_PORT = "18080" -$env:DPP_REGISTRY_PORT = "18081" -$env:MOCK_REPO_PORT = "18080" -$env:MOCK_REGISTRY_PORT = "18081" -$env:DPP_REPO_BASE_URL = "http://localhost:18080" -$env:DPP_REGISTRY_BASE_URL = "http://localhost:18081" +$env:DPP_REPO_PORT = "28080" +$env:DPP_REGISTRY_PORT = "28081" +$env:MOCK_REPO_PORT = "28080" +$env:MOCK_REGISTRY_PORT = "28081" +$env:DPP_REPO_BASE_URL = "http://localhost:28080" +$env:DPP_REGISTRY_BASE_URL = "http://localhost:28081" ``` Linux/macOS: ```bash -export DPP_REPO_PORT=18080 -export DPP_REGISTRY_PORT=18081 -export MOCK_REPO_PORT=18080 -export MOCK_REGISTRY_PORT=18081 -export DPP_REPO_BASE_URL=http://localhost:18080 -export DPP_REGISTRY_BASE_URL=http://localhost:18081 +export DPP_REPO_PORT=28080 +export DPP_REGISTRY_PORT=28081 +export MOCK_REPO_PORT=28080 +export MOCK_REGISTRY_PORT=28081 +export DPP_REPO_BASE_URL=http://localhost:28080 +export DPP_REGISTRY_BASE_URL=http://localhost:28081 +``` + +The example uses 28080 and 28081; replace them with any free port numbers. + +## Other service versions + +By default, the images set in [compose.yaml](compose.yaml) are used. To run against another +service version, set `$envFile` (PowerShell) or `env_file` (Bash) to one of the profiles in the +`env/` folder. Each file name states the version. The `pinned.env` profile pins exact image digests. +A missing profile file is reported as an error; nothing else is used silently in its place. +Keeping a profile in the repository does not guarantee that its images are publicly pullable. + +## Optional PowerShell lifecycle helper + +`manage-mock-services.ps1` checks the configuration, pulls missing images, waits until the +services are healthy, and keeps every action inside the selected project: + +```powershell +docker compose --env-file $envFile -f $composeFile -p $project pull +& (Join-Path $demoDir "manage-mock-services.ps1") -Action Start -EnvFile $envFile -Project $project +& (Join-Path $demoDir "manage-mock-services.ps1") -Action Status -EnvFile $envFile -Project $project +& (Join-Path $demoDir "manage-mock-services.ps1") -Action Logs -EnvFile $envFile -Project $project +& (Join-Path $demoDir "manage-mock-services.ps1") -Action Stop -EnvFile $envFile -Project $project ``` -Use different free ports when `18080` or `18081` is occupied. +`Stop` keeps the stored data. `Delete` removes it and requires `-ConfirmDelete`. Check the project +name before you use it. ## Project-scoped diagnosis -Never diagnose or clean up by an ambiguous default Compose project. Reuse the exact project, -Compose file, and environment file from startup. +Always use the same project name, compose file, and `.env` as when you started the services. +These commands show the containers, their logs, and the final configuration: PowerShell: @@ -78,22 +105,23 @@ docker compose -f "$compose_file" -p "$project" --env-file "$env_file" logs --no docker compose -f "$compose_file" -p "$project" --env-file "$env_file" config ``` -Common failure branches: +Common problems: -- Docker unavailable: start Docker Engine/Desktop. -- Pull rejected: verify access to the image registry and selected profile. -- Port collision: set all paired port/base-URL variables above, then recreate the project. -- Readiness blocked: preserve `ps` and logs before teardown. -- `demo` or `full` missing from help: force-reinstall the exact newly built SDK and demo wheels. -- Package provenance mismatch: rebuild and force-reinstall the SDK wheel passed with `--sdk-wheel`. -- Image identity finds no container: verify `up` and `verify` used the same project name and that the - installed demo package is current. +- **Docker is not available:** start Docker Desktop or Docker Engine. +- **Pull is rejected:** check that you can reach `ghcr.io` and that the selected profile exists. +- **Port is already in use:** see [alternate ports](#alternate-ports), then start the services again. +- **Services never become healthy:** save the `ps` and `logs` output before you stop anything. +- **`demo` is missing from `--help`:** reinstall the SDK and the demo package + (`python -m pip install --force-reinstall -e . -e ./examples/mock-services-demo`). +- **Package provenance mismatch:** rebuild and force-reinstall the SDK wheel passed with `--sdk-wheel`. +- **Image check finds no container:** make sure the services were started with the same project + name you pass to `verify`. ## Cleanup choices -The commands below affect only the named demo project. Do not substitute a shared project name. +These commands affect only the named demo project. -Stop/remove containers and the project network while keeping database volumes: +Stop and remove the containers, but keep the stored data: ```powershell docker compose -f $composeFile -p $project --env-file $envFile down --remove-orphans @@ -103,42 +131,121 @@ docker compose -f $composeFile -p $project --env-file $envFile down --remove-orp docker compose -f "$compose_file" -p "$project" --env-file "$env_file" down --remove-orphans ``` -Stop/remove the project and permanently delete its disposable database volumes: +Stop and remove everything, **including the stored data**: ```powershell docker compose -f $composeFile -p $project --env-file $envFile down --volumes --remove-orphans -Remove-Item -LiteralPath $report -ErrorAction SilentlyContinue ``` ```bash docker compose -f "$compose_file" -p "$project" --env-file "$env_file" down --volumes --remove-orphans -rm -f "$report" ``` -Copy a report elsewhere before removing `$report` or `"$report"` when evidence must be retained. +## Choose a verification level -## Legacy profile +- **SDK development:** run the root pytest suite with `-m "not integration"`. +- **Demo development:** run the demo pytest suite with `-m "not integration"`. +- **Connected integration:** `python -m dpp_mock_services_demo.maintainer live --env-file ` + runs the 19 live scenarios once. +- **Full release confidence:** use the full-verification flow below. It runs + both non-live suites, then strict clean-wheel verification and the 19 live scenarios once. -The `0.4.0.env` profile requires explicit `--legacy` on demo commands. Legacy runs are informational -and do not satisfy the maintained strict-verification gate. Keep their reports separate from current -`0.5.1.env` evidence. +## Optional maintainer verification -## CI and reports +Keep the connected Compose project running for these commands. -The manually triggered GitHub workflow `Mock services demo verification` builds both wheels, starts -a fresh disposable stack for the selected profile, runs installed-wheel verification, captures -evidence, and tears down its project. The PyPI release workflow does not start Mock images; it runs -SDK-only installed-wheel verification. +```powershell +python -m dpp_mock_services_demo.maintainer sdk-contracts +python -m dpp_mock_services_demo.maintainer live --env-file $envFile + +python -m build --outdir .\artifacts\root +$sdkWheel = (Get-ChildItem .\artifacts\root\dpp_sdk-*.whl | Select-Object -First 1).FullName +$report = Join-Path ([System.IO.Path]::GetTempPath()) "dpp-demo-verify-$PID.json" +python -m dpp_mock_services_demo.maintainer verify ` + --env-file $envFile --compose-project $project --sdk-wheel $sdkWheel --report-file $report +``` -Reports distinguish `PASSED`, `EXPECTED_ERROR`, `SKIPPED`, `FAILED`, and `NOT_IMPLEMENTED`. The live -educational report uses `PASS`, `EXPECTED_ERROR`, `BLOCKED`, `SKIP`, and `FAIL`. Keep educational -success separate from the strict verification verdict. +```bash +python -m dpp_mock_services_demo.maintainer sdk-contracts +python -m dpp_mock_services_demo.maintainer live --env-file "$env_file" + +mkdir -p artifacts/root +python -m build --outdir artifacts/root +set -- artifacts/root/dpp_sdk-*.whl +sdk_wheel="$1" +report="${TMPDIR:-/tmp}/dpp-demo-verify-$$.json" +python -m dpp_mock_services_demo.maintainer verify \ + --env-file "$env_file" --compose-project "$project" --sdk-wheel "$sdk_wheel" --report-file "$report" +``` -## Choose a verification level +## Full SDK and live service test suites -- **SDK development:** run the root pytest suite with `-m "not integration"`. -- **Demo development:** run the demo pytest suite with `-m "not integration"`. -- **Connected integration:** `python -m dpp_mock_services_demo.maintainer live --env-file ` - runs the 22 live scenarios once. -- **Full release confidence:** use the README full-verification flow. It runs - both non-live suites, then strict clean-wheel verification and the 22 live scenarios once. +Keep the mock services running and use the same environment file for both +commands. Together they run the full root SDK suite, the complete demo suite, +and every opt-in live test. + +```powershell +$env:DPP_DEMO_ENV_FILE = (Resolve-Path $envFile).Path +$demoConfig = Get-Content $envFile -Raw | ConvertFrom-StringData +$env:DPP_REPO_BASE_URL = $demoConfig.DPP_REPO_BASE_URL +$env:DPP_REGISTRY_BASE_URL = $demoConfig.DPP_REGISTRY_BASE_URL + +$artifacts = Join-Path (Resolve-Path .).Path "artifacts\verification" +New-Item -ItemType Directory -Force -Path $artifacts | Out-Null +python -m build --outdir $artifacts +python -m build .\examples\mock-services-demo --outdir $artifacts +$sdkWheel = (Get-ChildItem $artifacts\dpp_sdk-*.whl | Select-Object -First 1).FullName +$demoWheel = (Get-ChildItem $artifacts\dpp_sdk_mock_services_demo-*.whl | Select-Object -First 1).FullName +$verifyVenv = Join-Path (Resolve-Path .).Path ".verify-venv" +if (-not (Test-Path (Join-Path $verifyVenv "Scripts\python.exe"))) { + python -m venv $verifyVenv +} +$verifyPython = Join-Path $verifyVenv "Scripts\python.exe" +& $verifyPython -m pip install --upgrade pip +& $verifyPython -m pip install --force-reinstall $sdkWheel $demoWheel + +python -m pytest .\tests --run-mock-services --force-sugar +python -m pytest -c .\examples\mock-services-demo\pyproject.toml ` + .\examples\mock-services-demo\tests ` + --run-mock-services --force-sugar +$report = Join-Path ([System.IO.Path]::GetTempPath()) "dpp-demo-verify-$PID.json" +Push-Location ([System.IO.Path]::GetTempPath()) +try { + & $verifyPython -I -m dpp_mock_services_demo.maintainer verify ` + --env-file $envFile --compose-project $project --sdk-wheel $sdkWheel ` + --report-file $report --summary +} finally { + Pop-Location +} +``` + +```bash +DPP_DEMO_ENV_FILE="$env_file" +export DPP_DEMO_ENV_FILE +export DPP_REPO_BASE_URL="$(sed -n 's/^DPP_REPO_BASE_URL=//p' "$env_file" | head -n 1)" +export DPP_REGISTRY_BASE_URL="$(sed -n 's/^DPP_REGISTRY_BASE_URL=//p' "$env_file" | head -n 1)" + +artifacts=./artifacts/verification +mkdir -p "$artifacts" +python -m build --outdir "$artifacts" +python -m build ./examples/mock-services-demo --outdir "$artifacts" +set -- "$artifacts"/dpp_sdk-*.whl +sdk_wheel="$1" +set -- "$artifacts"/dpp_sdk_mock_services_demo-*.whl +demo_wheel="$1" +verify_venv=.verify-venv +[ -x "$verify_venv/bin/python" ] || python -m venv "$verify_venv" +verify_python="$verify_venv/bin/python" +"$verify_python" -m pip install --upgrade pip +"$verify_python" -m pip install --force-reinstall "$sdk_wheel" "$demo_wheel" + +python -m pytest ./tests --run-mock-services --force-sugar +python -m pytest \ + -c ./examples/mock-services-demo/pyproject.toml \ + ./examples/mock-services-demo/tests \ + --run-mock-services --force-sugar +report="${TMPDIR:-/tmp}/dpp-demo-verify-$$.json" +(cd "${TMPDIR:-/tmp}" && "$verify_python" -I -m dpp_mock_services_demo.maintainer verify \ + --env-file "$env_file" --compose-project "$project" --sdk-wheel "$sdk_wheel" \ + --report-file "$report" --summary) +``` diff --git a/examples/mock-services-demo/README.md b/examples/mock-services-demo/README.md index 05c9101..24b0066 100644 --- a/examples/mock-services-demo/README.md +++ b/examples/mock-services-demo/README.md @@ -1,188 +1,99 @@ -# Python SDK repository/registry services demo +# Mock-services demo -This consumer example uses the public Python SDK against disposable repository -and registry mock-service images. It does not add service behavior to the SDK. +This demo shows the Python SDK at work in two steps: -**Run from:** the Python repository root. Python 3.11+ is required. Docker is -needed only for connected commands. +1. **Offline walkthrough:** five SDK scenarios that need no services and no Docker. +2. **Connected demo:** the SDK talks to example Java repository and registry services that run + locally in Docker. The service images are set in [compose.yaml](compose.yaml). + +The demo is optional and separate from the SDK. You don't need it to use `dpp-sdk`. + +## Before you start + +- Python 3.11 or newer. +- For the connected demo only: [Docker](https://docs.docker.com/get-started/get-docker/) with + Compose v2 (on Windows and macOS: Docker Desktop, started). +- A clone of this repository with an activated development environment. See + [Set up the development environment](../../CONTRIBUTING.md#set-up-the-development-environment). + +**Run from:** the repository root (the folder that contains `pyproject.toml`) for every command +on this page. ## Quick start -Run the offline SDK walkthrough. The offline example does not load a service profile or contact -Docker. It covers representative `SDK-01`, `SDK-02`, `SDK-03`, `SDK-06`, and `SDK-07` scenarios. -Read the annotated [sdk_scenarios.py](src/dpp_mock_services_demo/sdk_scenarios.py) -source to see the SDK calls behind those cases. +Install the demo and run the offline walkthrough. The offline example does not load a service +profile or contact Docker: -```powershell -if (-not (Test-Path .\.venv\Scripts\python.exe)) { python -m venv .venv } -& .\.venv\Scripts\python.exe -m pip install -e ".[dev,release]" -& .\.venv\Scripts\python.exe -m pip install -e .\examples\mock-services-demo -& .\.venv\Scripts\python.exe -m dpp_mock_services_demo sdk +```bash +python -m pip install -e ./examples/mock-services-demo +python -m dpp_mock_services_demo sdk ``` +To see the SDK calls behind each scenario, read the commented +[sdk_scenarios.py](src/dpp_mock_services_demo/sdk_scenarios.py). + ## Optional connected demo -Create one local environment file. Select unique paired ports and base URLs in -that file before starting services. +First check that Docker is running: -```powershell -$demoDir = (Resolve-Path .\examples\mock-services-demo).Path -$envFile = Join-Path $demoDir ".env" -if (-not (Test-Path -LiteralPath $envFile)) { - Copy-Item (Join-Path $demoDir ".env.example") $envFile -} +```bash +docker compose version ``` -Start services, run the connected demo, then remove the disposable project. -Cleanup is project-scoped because every Compose command uses `$project`: -Compose creates project-prefixed container names for this isolated stack. -Docker Engine or Docker Desktop must be running before these commands. -Read the annotated [integration_scenarios.py](src/dpp_mock_services_demo/integration_scenarios.py) -source to see the public client calls and cleanup sequence. +You should see a version number. If you see an error, start Docker Desktop (or Docker Engine) first. + +The commands below: + +1. create your local settings file `.env` from `.env.example`, if it doesn't exist yet. It sets + the ports, 18080 for the repository and 18081 for the registry (the README quickstart runs + without `.env` and uses 8080 and 8081, so both can run side by side); +2. start the services; +3. run the demo; +4. stop the services. + +Every command uses the same project name, so cleanup only affects this demo (project-scoped). + +### PowerShell ```powershell +$demoDir = (Resolve-Path .\examples\mock-services-demo).Path +$envFile = Join-Path $demoDir ".env" $composeFile = Join-Path $demoDir "compose.yaml" $project = "dpp-mock-services-demo-local" +if (-not (Test-Path -LiteralPath $envFile)) { Copy-Item (Join-Path $demoDir ".env.example") $envFile } + docker compose --env-file $envFile -f $composeFile -p $project pull docker compose --env-file $envFile -f $composeFile -p $project up -d --wait -& .\.venv\Scripts\python.exe -m dpp_mock_services_demo demo --env-file $envFile -docker compose --env-file $envFile -f $composeFile -p $project down -v +python -m dpp_mock_services_demo demo --env-file $envFile +docker compose --env-file $envFile -f $composeFile -p $project down ``` -The direct Compose commands are cross-platform. The PowerShell-only -`manage-mock-services.ps1` is an Optional PowerShell convenience wrapper and -does not depend on PowerShell script execution policy beyond the current process: - -```powershell -Set-ExecutionPolicy -Scope Process Bypass -& (Join-Path $demoDir "manage-mock-services.ps1") -Action Start -EnvFile $envFile -``` - - -## Linux/macOS - -Run the same commands from Bash or a compatible shell. +### Bash (Linux/macOS) ```bash -if [ ! -x .venv/bin/python ]; then python3 -m venv .venv; fi -.venv/bin/python -m pip install -e ".[dev,release]" -.venv/bin/python -m pip install -e ./examples/mock-services-demo -.venv/bin/python -m dpp_mock_services_demo sdk - demo_dir="$(cd ./examples/mock-services-demo && pwd)" env_file="$demo_dir/.env" -if [ ! -f "$env_file" ]; then - cp "$demo_dir/.env.example" "$env_file" -fi - compose_file="$demo_dir/compose.yaml" project="dpp-mock-services-demo-local" +[ -f "$env_file" ] || cp "$demo_dir/.env.example" "$env_file" + docker compose --env-file "$env_file" -f "$compose_file" -p "$project" pull docker compose --env-file "$env_file" -f "$compose_file" -p "$project" up -d --wait -.venv/bin/python -m dpp_mock_services_demo demo --env-file "$env_file" -docker compose --env-file "$env_file" -f "$compose_file" -p "$project" down -v +python -m dpp_mock_services_demo demo --env-file "$env_file" +docker compose --env-file "$env_file" -f "$compose_file" -p "$project" down ``` -## Optional maintainer verification +`down` stops the services but keeps their stored data. To delete the data too, add `--volumes`. -Keep the connected Compose project running for these commands. +To see the client calls and the cleanup order, read the commented +[integration_scenarios.py](src/dpp_mock_services_demo/integration_scenarios.py). -```powershell -& .\.venv\Scripts\python.exe -m dpp_mock_services_demo.maintainer sdk-contracts -& .\.venv\Scripts\python.exe -m dpp_mock_services_demo.maintainer live --env-file $envFile - -& .\.venv\Scripts\python.exe -m build --outdir .\artifacts\root -$sdkWheel = (Get-ChildItem .\artifacts\root\dpp_sdk-*.whl | Select-Object -First 1).FullName -$report = Join-Path ([System.IO.Path]::GetTempPath()) "dpp-demo-verify-$PID.json" -& .\.venv\Scripts\python.exe -m dpp_mock_services_demo.maintainer verify ` - --env-file $envFile --compose-project $project --sdk-wheel $sdkWheel --report-file $report -``` +## If something goes wrong -```bash -.venv/bin/python -m dpp_mock_services_demo.maintainer sdk-contracts -.venv/bin/python -m dpp_mock_services_demo.maintainer live --env-file "$env_file" - -mkdir -p artifacts/root -.venv/bin/python -m build --outdir artifacts/root -set -- artifacts/root/dpp_sdk-*.whl -sdk_wheel="$1" -report="${TMPDIR:-/tmp}/dpp-demo-verify-$$.json" -.venv/bin/python -m dpp_mock_services_demo.maintainer verify \ - --env-file "$env_file" --compose-project "$project" --sdk-wheel "$sdk_wheel" --report-file "$report" -``` - -## Full SDK and live service test suites - -Keep the mock services running and use the same environment file for both -commands. Together they run the full root SDK suite, the complete demo suite, -and every opt-in live test. - -```powershell -$env:DPP_DEMO_ENV_FILE = (Resolve-Path $envFile).Path -$demoConfig = Get-Content $envFile -Raw | ConvertFrom-StringData -$env:DPP_REPO_BASE_URL = $demoConfig.DPP_REPO_BASE_URL -$env:DPP_REGISTRY_BASE_URL = $demoConfig.DPP_REGISTRY_BASE_URL - -$artifacts = Join-Path (Resolve-Path .).Path "artifacts\verification" -New-Item -ItemType Directory -Force -Path $artifacts | Out-Null -& .\.venv\Scripts\python.exe -m build --outdir $artifacts -& .\.venv\Scripts\python.exe -m build .\examples\mock-services-demo --outdir $artifacts -$sdkWheel = (Get-ChildItem $artifacts\dpp_sdk-*.whl | Select-Object -First 1).FullName -$demoWheel = (Get-ChildItem $artifacts\dpp_sdk_mock_services_demo-*.whl | Select-Object -First 1).FullName -$verifyVenv = Join-Path (Resolve-Path .).Path ".verify-venv" -if (-not (Test-Path (Join-Path $verifyVenv "Scripts\python.exe"))) { - & .\.venv\Scripts\python.exe -m venv $verifyVenv -} -$verifyPython = Join-Path $verifyVenv "Scripts\python.exe" -& $verifyPython -m pip install --upgrade pip -& $verifyPython -m pip install --force-reinstall $sdkWheel $demoWheel - -& .\.venv\Scripts\python.exe -m pytest .\tests --run-mock-services --force-sugar -& .\.venv\Scripts\python.exe -m pytest -c .\examples\mock-services-demo\pyproject.toml ` - .\examples\mock-services-demo\tests ` - --run-mock-services --force-sugar -$report = Join-Path ([System.IO.Path]::GetTempPath()) "dpp-demo-verify-$PID.json" -Push-Location ([System.IO.Path]::GetTempPath()) -try { - & $verifyPython -I -m dpp_mock_services_demo.maintainer verify ` - --env-file $envFile --compose-project $project --sdk-wheel $sdkWheel ` - --report-file $report --summary -} finally { - Pop-Location -} -``` - -```bash -DPP_DEMO_ENV_FILE="$env_file" -export DPP_DEMO_ENV_FILE -export DPP_REPO_BASE_URL="$(sed -n 's/^DPP_REPO_BASE_URL=//p' "$env_file" | head -n 1)" -export DPP_REGISTRY_BASE_URL="$(sed -n 's/^DPP_REGISTRY_BASE_URL=//p' "$env_file" | head -n 1)" - -artifacts=./artifacts/verification -mkdir -p "$artifacts" -.venv/bin/python -m build --outdir "$artifacts" -.venv/bin/python -m build ./examples/mock-services-demo --outdir "$artifacts" -set -- "$artifacts"/dpp_sdk-*.whl -sdk_wheel="$1" -set -- "$artifacts"/dpp_sdk_mock_services_demo-*.whl -demo_wheel="$1" -verify_venv=.verify-venv -[ -x "$verify_venv/bin/python" ] || .venv/bin/python -m venv "$verify_venv" -verify_python="$verify_venv/bin/python" -"$verify_python" -m pip install --upgrade pip -"$verify_python" -m pip install --force-reinstall "$sdk_wheel" "$demo_wheel" - -.venv/bin/python -m pytest ./tests --run-mock-services --force-sugar -.venv/bin/python -m pytest \ - -c ./examples/mock-services-demo/pyproject.toml \ - ./examples/mock-services-demo/tests \ - --run-mock-services --force-sugar -report="${TMPDIR:-/tmp}/dpp-demo-verify-$$.json" -(cd "${TMPDIR:-/tmp}" && "$verify_python" -I -m dpp_mock_services_demo.maintainer verify \ - --env-file "$env_file" --compose-project "$project" --sdk-wheel "$sdk_wheel" \ - --report-file "$report" --summary) -``` +- **Port already in use:** change the ports in `.env`. See + [alternate ports](ADVANCED_OPERATIONS.md#alternate-ports). +- **`docker compose up` fails or hangs:** see + [project-scoped diagnosis](ADVANCED_OPERATIONS.md#project-scoped-diagnosis). -For extra lifecycle commands, troubleshooting, alternate profiles, logs, and -cleanup options, see [ADVANCED_OPERATIONS.md](ADVANCED_OPERATIONS.md). This is -an optional troubleshooting reference, not the normal demo path. +For logs, other service versions, and maintainer verification, see +[ADVANCED_OPERATIONS.md](ADVANCED_OPERATIONS.md). diff --git a/examples/mock-services-demo/compose.yaml b/examples/mock-services-demo/compose.yaml index b2fc05e..bf32450 100644 --- a/examples/mock-services-demo/compose.yaml +++ b/examples/mock-services-demo/compose.yaml @@ -32,7 +32,7 @@ services: retries: 5 dpp-repo-api: - image: ${DPP_REPO_IMAGE:?set DPP_REPO_IMAGE} + image: ${DPP_REPO_IMAGE:-ghcr.io/cir4fun-eu/dpp-repo-api:0.6.0} depends_on: dpp-repo-db: condition: service_healthy @@ -47,7 +47,7 @@ services: - "${MOCK_REPO_PORT:-8080}:${MOCK_REPO_PORT:-8080}" dpp-registry-api: - image: ${DPP_REGISTRY_IMAGE:?set DPP_REGISTRY_IMAGE} + image: ${DPP_REGISTRY_IMAGE:-ghcr.io/cir4fun-eu/dpp-registry-api:0.6.0} depends_on: dpp-repo-api: condition: service_started diff --git a/examples/mock-services-demo/env/0.5.1.env b/examples/mock-services-demo/env/0.5.1.env index 09fdb5a..c99bb06 100644 --- a/examples/mock-services-demo/env/0.5.1.env +++ b/examples/mock-services-demo/env/0.5.1.env @@ -1,4 +1,4 @@ -# Current maintained mock service images. Pull these only after version 0.5.1 is published. +# Versioned mock-service image references for the 0.5.1 profile. DPP_REPO_IMAGE=ghcr.io/cir4fun-eu/dpp-repo-api:0.5.1 DPP_REGISTRY_IMAGE=ghcr.io/cir4fun-eu/dpp-registry-api:0.5.1 DPP_REPO_BASE_URL=http://localhost:8080 diff --git a/examples/mock-services-demo/pyproject.toml b/examples/mock-services-demo/pyproject.toml index c9d2377..2ff3da4 100644 --- a/examples/mock-services-demo/pyproject.toml +++ b/examples/mock-services-demo/pyproject.toml @@ -8,7 +8,7 @@ version = "0.1.0" description = "Python SDK demo against repository and registry HTTP services; supplied profiles currently use mock service images" readme = "README.md" requires-python = ">=3.11" -dependencies = ["dpp-sdk==0.4.0", "httpx>=0.27"] +dependencies = ["dpp-sdk==0.5.0", "httpx>=0.27"] [project.optional-dependencies] dev = ["pytest>=8.0", "pytest-sugar>=1.1.1", "ruff>=0.4"] diff --git a/examples/mock-services-demo/src/dpp_mock_services_demo/controlled_scenarios.py b/examples/mock-services-demo/src/dpp_mock_services_demo/controlled_scenarios.py index 56aec3b..f33deb4 100644 --- a/examples/mock-services-demo/src/dpp_mock_services_demo/controlled_scenarios.py +++ b/examples/mock-services-demo/src/dpp_mock_services_demo/controlled_scenarios.py @@ -1,16 +1,15 @@ -"""Deterministic public-client contracts that are unsafe or impossible to induce live.""" +"""Deterministic client contract examples that need no live services.""" from __future__ import annotations import json from collections.abc import Callable -from datetime import UTC, datetime, timedelta, timezone +from datetime import datetime, timedelta, timezone from time import perf_counter from typing import Any from uuid import UUID import httpx -from dpp_sdk import Dpp4FunJsonCodec, OrganizationRole, to_json, validate_dpp4fun from dpp_sdk.clients import ( DppApiClientError, DppHttpClientError, @@ -18,7 +17,7 @@ DppNetworkClientError, DppRegistryClient, DppRepoClient, - DppValidationClientError, + DppStatusCode, RegisterDppRequest, ) @@ -30,39 +29,31 @@ def _result(scenario_id: str, name: str, operation: Callable[[], str]) -> Scenar started = perf_counter() try: details = operation() - except Exception as exc: # noqa: BLE001 - controlled boundary records unexpected category + except Exception as exc: # noqa: BLE001 - report unexpected categories at this boundary return ScenarioResult( - scenario_id=scenario_id, - name=name, - category="CONTROLLED", - status=ScenarioStatus.FAILED, - duration_seconds=perf_counter() - started, - summary="Controlled client assertion failed", - details=f"{type(exc).__name__}: {exc}", + scenario_id, + name, + "CONTROLLED", + ScenarioStatus.FAILED, + perf_counter() - started, + "Controlled client assertion failed", + f"{type(exc).__name__}: {exc}", ) return ScenarioResult( - scenario_id=scenario_id, - name=name, - category="CONTROLLED", - status=ScenarioStatus.PASSED, - duration_seconds=perf_counter() - started, - summary="Controlled client assertion passed", - details=details, + scenario_id, + name, + "CONTROLLED", + ScenarioStatus.PASSED, + perf_counter() - started, + "Controlled client assertion passed", + details, ) -def _expect( - error_type: type[BaseException], - operation: Callable[[], object], - *, - cause_type: type[BaseException] | None = None, -) -> BaseException: +def _expect(error_type: type[BaseException], operation: Callable[[], object]) -> BaseException: try: operation() except error_type as exc: - if cause_type is not None and not isinstance(exc.__cause__, cause_type): - actual = type(exc.__cause__).__name__ if exc.__cause__ is not None else "None" - raise AssertionError(f"expected cause {cause_type.__name__}, got {actual}") from exc return exc raise AssertionError(f"expected {error_type.__name__}") @@ -71,145 +62,83 @@ def _identity() -> DemoIdentity: return DemoIdentity.from_run_id(UUID("12345678-1234-5678-9234-567812345678")) -def _rep_16() -> str: - fixture = build_complete_fixture(_identity()) - supplier = fixture.supplier - assert supplier is not None - invalid_supplier = supplier.with_updates(role=OrganizationRole.MANUFACTURER) - invalid_nameplate = fixture.nameplate.with_updates(supplier=invalid_supplier) - invalid = fixture.with_updates( - coreDpp=fixture.coreDpp.with_updates(nameplate=invalid_nameplate) - ) - requests = 0 - - def handler(_request: httpx.Request) -> httpx.Response: - nonlocal requests - requests += 1 - return httpx.Response(500) - - with httpx.Client(transport=httpx.MockTransport(handler)) as http: - repo = DppRepoClient( - "http://repo.test", - Dpp4FunJsonCodec(), - validate_dpp4fun, - client=http, - ) - _expect(DppValidationClientError, lambda: repo.create_dpp(invalid)) - if requests != 0: - raise AssertionError(f"validation failure sent {requests} request(s)") - return "invalid supplier role became DppValidationClientError with zero requests" - - -def _path_timestamp_and_body_contracts() -> None: - fixture = build_complete_fixture(_identity()) - encoded = b"*%7E%20%2F%2B%25Gr%C3%BC%C3%9Fe" - value = "*~ /+%Grüße" - observed_paths: list[bytes] = [] - observed_bodies: list[bytes] = [] +def _path_timestamp_and_body_contracts() -> str: + observed: list[tuple[bytes, bytes]] = [] def handler(request: httpx.Request) -> httpx.Response: - observed_paths.append(request.url.raw_path) - observed_bodies.append(request.content) - if "dppsByIdAndDate" in request.url.path: - payload: Any = json.loads(to_json(fixture)) + observed.append((request.url.raw_path, request.content)) + if request.url.path.endswith("/dppsByProductIds"): + payload: Any = {"dppIdentifiers": ["dpp-1"], "nextCursor": None} + elif request.method == "GET": + payload = {"digitalProductPassportId": "DPP 1"} else: payload = "accepted" return httpx.Response(200, json={"statusCode": "Success", "payload": payload}) with httpx.Client(transport=httpx.MockTransport(handler)) as http: - repo = DppRepoClient( - "http://repo.test", - Dpp4FunJsonCodec(), - validate_dpp4fun, - client=http, - ) - repo.read_data_element(value, value) + repo = DppRepoClient("http://repo.test", client=http) + repo.read_data_element("DPP 1", "$.characteristics.color") repo.read_dpp_version_by_id_and_date( "DPP 1", datetime(2026, 1, 15, 14, 0, 0, 123000, tzinfo=timezone(timedelta(hours=2))), ) - repo.update_data_element("D", "$.characteristics.productName", "scalar") - repo.update_data_element("D", "$.characteristics.color", None) - - expected_element = b"/v1/dpps/" + encoded + b"/elements/" + encoded - if observed_paths[0] != expected_element: - raise AssertionError(f"unexpected encoded path {observed_paths[0]!r}") - expected_history = ( - b"/v1/dppsByIdAndDate/DPP%201?date=2026-01-15T12%3A00%3A00.123Z&representation=full" - ) - if observed_paths[1] != expected_history: - raise AssertionError(f"unexpected timestamp path {observed_paths[1]!r}") - if observed_bodies[2:] != [b'"scalar"', b"null"]: - raise AssertionError(f"unexpected fine update bodies {observed_bodies[2:]!r}") - - -class _NullCodec: - def to_json(self, _value: object) -> str: - return "{}" - - def from_json(self, _raw: str) -> object: - return None # type: ignore[return-value] + repo.update_data_element("D", "$.characteristics.color", "green") + repo.read_dpp_ids_by_product_ids(["product-1"], limit=10, cursor="0") + + assert observed[0][0] == b"/v1/dpps/DPP%201/elements/%24.characteristics.color" + assert observed[1][0] == (b"/v1/dppsByIdAndDate/DPP%201?date=2026-01-15T12%3A00%3A00.123000Z") + assert observed[2][1] == b'"green"' + assert json.loads(observed[3][1]) == { + "productIdentifiers": ["product-1"], + "limit": 10, + "cursor": "0", + } + return "element, history, patch, and bulk lookup request contracts verified" -def _error_pipeline_contracts() -> None: - def repo_for( - handler: Callable[[httpx.Request], httpx.Response], - ) -> tuple[DppRepoClient[Any], httpx.Client]: +def _error_pipeline_contracts() -> str: + def client_for(handler: Callable[[httpx.Request], httpx.Response]): http = httpx.Client(transport=httpx.MockTransport(handler)) - repo = DppRepoClient( - "http://repo.test", - _NullCodec(), - lambda _value: None, - client=http, - ) - return repo, http + return DppRepoClient("http://repo.test", client=http), http - repo, http = repo_for(lambda _request: httpx.Response(503, text="unavailable")) + repo, http = client_for(lambda _request: httpx.Response(503, text="unavailable")) try: error = _expect(DppHttpClientError, lambda: repo.read_dpp_by_id("D")) assert isinstance(error, DppHttpClientError) and error.status_code == 503 finally: http.close() - repo, http = repo_for( + repo, http = client_for( lambda _request: httpx.Response( - 200, - json={"statusCode": "ClientErrorBadRequest", "payload": {}}, + 200, json={"statusCode": "ClientErrorBadRequest", "message": []} ) ) try: - _expect(DppApiClientError, lambda: repo.read_dpp_by_id("D")) + error = _expect(DppApiClientError, lambda: repo.read_dpp_by_id("D")) + assert isinstance(error, DppApiClientError) + assert error.status_code is DppStatusCode.ClientErrorBadRequest finally: http.close() - mapping_handlers = ( + for handler in ( lambda _request: httpx.Response(200, text="not-json"), lambda _request: httpx.Response(200, json={"payload": {}}), - lambda _request: httpx.Response(200, json={"statusCode": "Success"}), - lambda _request: httpx.Response(200, json={"statusCode": "Success", "payload": {}}), - ) - for index, handler in enumerate(mapping_handlers): - repo, http = repo_for(handler) + ): + repo, http = client_for(handler) try: - error = _expect( + _expect( DppMappingClientError, lambda current_repo=repo: current_repo.read_dpp_by_id("D"), ) - if error.__cause__ is None: - raise AssertionError(f"mapping case {index} has no cause") finally: http.close() def network_handler(request: httpx.Request) -> httpx.Response: raise httpx.ConnectError("controlled network failure", request=request) - repo, http = repo_for(network_handler) + repo, http = client_for(network_handler) try: - _expect( - DppNetworkClientError, - lambda: repo.read_dpp_by_id("D"), - cause_type=httpx.ConnectError, - ) + _expect(DppNetworkClientError, lambda: repo.read_dpp_by_id("D")) finally: http.close() @@ -220,95 +149,77 @@ def registry_handler(_request: httpx.Request) -> httpx.Response: sent += 1 return httpx.Response(500) - with httpx.Client(transport=httpx.MockTransport(registry_handler)) as registry_http: - registry = DppRegistryClient("http://registry.test", client=registry_http) - _expect( - DppMappingClientError, - lambda: registry.post_new_dpp_to_registry(None), # type: ignore[arg-type] - cause_type=ValueError, - ) - if sent: - raise AssertionError("invalid registry request reached transport") - - -def _rep_17() -> str: - _path_timestamp_and_body_contracts() - _error_pipeline_contracts() - return ( - "exact path/timestamp/body wire; HTTP/API/mapping/network categories; " - "null payload/codec and invalid registry request causes" - ) + with httpx.Client(transport=httpx.MockTransport(registry_handler)) as http: + registry = DppRegistryClient("http://registry.test", client=http) + _expect(DppMappingClientError, lambda: registry.post_new_dpp_to_registry(None)) # type: ignore[arg-type] + assert sent == 0 + return "HTTP, API, mapping, network, and preflight input failures verified" -def _rep_18() -> str: - fixture = build_complete_fixture(_identity()) +def _versioned_endpoint_and_identity() -> str: observed: list[bytes] = [] def handler(request: httpx.Request) -> httpx.Response: observed.append(request.url.raw_path) return httpx.Response( 200, - json={"statusCode": "Success", "payload": json.loads(to_json(fixture))}, + json={"statusCode": "Success", "payload": {"dppIdentifiers": [], "nextCursor": None}}, ) with httpx.Client(transport=httpx.MockTransport(handler)) as http: - repo = DppRepoClient( - "http://repo.test", - Dpp4FunJsonCodec(), - validate_dpp4fun, - client=http, + DppRepoClient("http://repo.test", client=http).read_dpp_ids_by_product_ids( + ["Product 1", "x/y"], limit=5 ) - repo.read_dpp_version_by_product_id_and_date( - "Product 1", - datetime(2026, 1, 15, 12, 0, tzinfo=UTC), - ) - expected = b"/dppsByProductIdAndDate/Product%201?date=2026-01-15T12%3A00%3A00Z" - if observed != [expected]: - raise AssertionError(f"unexpected legacy route {observed!r}") - return "legacy product-history route shape verified without a Mock-image call" + if observed != [b"/v1/dppsByProductIds"]: + raise AssertionError(f"unexpected product batch route {observed!r}") + return "versioned batch lookup route and JSON body verified" + +def _rep17() -> str: + paths = _path_timestamp_and_body_contracts() + errors = _error_pipeline_contracts() + return f"{paths}; {errors}" -def _reg_08() -> str: - observed_keys: list[set[str]] = [] + +def _registry_wire_contract() -> str: + observed: list[bytes] = [] def handler(request: httpx.Request) -> httpx.Response: - observed_keys.append(set(json.loads(request.content))) + observed.append(request.content) return httpx.Response( 200, - json={ - "statusCode": "SuccessCreated", - "payload": {"registrationId": "registration-1"}, - }, + json={"statusCode": "SuccessCreated", "payload": {"registrationId": "registration-1"}}, ) request = RegisterDppRequest( - uniqueProductIdentifier="product-1", - digitalProductPassportId="dpp-1", - uniqueEconomicOperatorIdentifier="operator-1", - dppApiEndpoint="http://repo.test", + unique_product_identifier="product-1", + digital_product_passport_id="dpp-1", + unique_economic_operator_identifier="operator-1", + dpp_api_endpoint="http://repo.test/v1/dpps", ) with httpx.Client(transport=httpx.MockTransport(handler)) as http: - registry = DppRegistryClient("http://registry.test", client=http) - response = registry.post_new_dpp_to_registry(request) - expected = { - "uniqueProductIdentifier", - "digitalProductPassportId", - "uniqueEconomicOperatorIdentifier", - "dppApiEndpoint", - } - if observed_keys != [expected]: - raise AssertionError(f"unexpected registry request keys {observed_keys!r}") - if response.registrationId != "registration-1": - raise AssertionError("canonical registrationId was not decoded") - return "four canonical request keys and registrationId response verified" + response = DppRegistryClient("http://registry.test", client=http).post_new_dpp_to_registry( + request + ) + if json.loads(observed[0]) != request.model_dump(mode="json", by_alias=True): + raise AssertionError("registry request did not preserve canonical JSON fields") + if response.registration_id != "registration-1": + raise AssertionError("registration response was not decoded") + return "canonical registration request and registrationId response verified" -def run_controlled_scenarios() -> tuple[ScenarioResult, ...]: - """Run approved deterministic transport/client assertions without external sockets.""" +def _semantic_boundary() -> str: + fixture = build_complete_fixture(_identity()) + if not fixture.passport.data_elements: + raise AssertionError("typed aggregate lost its core elements") + return "typed DPP4Fun remains an application concern outside the JSON client" + +def run_controlled_scenarios() -> tuple[ScenarioResult, ...]: + """Run exact HTTP behavior assertions without external sockets.""" return ( - _result("REP-16", "Local create validation before I/O", _rep_16), - _result("REP-17", "Exact wire and client error pipeline", _rep_17), - _result("REP-18", "Legacy product-history route classification", _rep_18), - _result("REG-08", "Canonical registry request and response keys", _reg_08), + _result("REP-16", "Typed model remains outside the JSON client", _semantic_boundary), + _result("REP-17", "Exact routes, bodies and error categories", _rep17), + _result("REP-18", "Versioned product batch endpoint", _versioned_endpoint_and_identity), + _result("REG-08", "Canonical registry request and response", _registry_wire_contract), ) diff --git a/examples/mock-services-demo/src/dpp_mock_services_demo/fixtures.py b/examples/mock-services-demo/src/dpp_mock_services_demo/fixtures.py index ab7adf5..87d49c1 100644 --- a/examples/mock-services-demo/src/dpp_mock_services_demo/fixtures.py +++ b/examples/mock-services-demo/src/dpp_mock_services_demo/fixtures.py @@ -1,36 +1,76 @@ -"""Deterministic demo fixtures with unique identities for every run.""" +"""Deterministic typed DPP4Fun demo fixtures with run-scoped identifiers.""" from __future__ import annotations from dataclasses import dataclass -from datetime import date +from datetime import UTC, datetime from uuid import UUID, uuid4 -from dpp_sdk import ( - Address, +from dpp_sdk import Dpp4Fun, DppHeader, Granularity +from dpp_sdk.core import DataElementCollection, SingleValuedDataElement +from dpp_sdk.dpp4fun import ( + Accessory, BillOfMaterials, - Characteristics, - Component, - Contact, - Dimensions, - Documentation, - Dpp4Fun, - DppCore, - Email, + CarbonFootprint, + CarbonFootprintCalculation, + Certificate, + ChairCharacteristics, + DesignRecord, + Dpp4FunQuantity, + Durability, + EnvironmentalImpact, + HealthImpact, + HealthIndicator, + Instruction, + LengthUnit, Material, Nameplate, - Organization, - OrganizationRole, - Part, - PassportMetadata, - ProductClassification, - Telephone, + PostSaleService, + ProductRelationship, + Recyclability, + Refurbishment, + Reliability, + Remanufacturing, + Repairability, + RepairRecord, + Reusability, + RightOfEndUsers, + Safety, + StandardAssessment, + Traceability, + Upgradability, + Usage, + WasteManagement, + profile_definition, ) +def _profile_collection(path: str, **values: object) -> DataElementCollection: + """Create a profile-backed composite for a source family without a named Python type.""" + definition = profile_definition(path) + children = tuple( + SingleValuedDataElement( + element_id=field.element_id, + dictionary_reference=field.dictionary_reference, + value_data_type=field.value_data_type, + value=value, + ) + for name, value in values.items() + if (field := profile_definition(f"{path}.{name}")).object_type == "SingleValuedDataElement" + ) + if len(children) != len(values): + raise ValueError(f"{path} accepts only the supplied scalar profile fields") + return DataElementCollection( + element_id=definition.element_id, + object_type="DataElementCollection", + dictionary_reference=definition.dictionary_reference, + elements=children, + ) + + @dataclass(frozen=True) class DemoIdentity: - """Run-scoped identifiers used by SDK and future live scenarios.""" + """Run-scoped identifiers used by SDK and live scenarios.""" run_id: UUID dpp_id: UUID @@ -54,111 +94,173 @@ def new_demo_identity() -> DemoIdentity: return DemoIdentity.from_run_id(uuid4()) -def _manufacturer(*, complete: bool) -> Organization: - if not complete: - return Organization(name="CIR4FUN Demo Manufacturer", role=OrganizationRole.MANUFACTURER) - return Organization( - name="CIR4FUN Demo Manufacturer", - gln="4012345000009", - productDescription="Deterministic Mock-services demo fixture", - productDesignation="Demo Office Chair", - productFamily="Furniture", - productRoot="Chair", - productOrderSuffix="DEMO", - uri="https://example.invalid/manufacturers/cir4fun-demo", - role=OrganizationRole.MANUFACTURER, - contact=Contact( - organization="CIR4FUN Demo Support", - address=Address( - country="DE", - zipCode="10115", - region="Berlin", - town="Berlin", - street="Example Street 1", - ), - email=Email(emailAddress="support@example.invalid", typeOfEmail="business"), - telephone=Telephone(telephoneNumber="+49-30-5550000", typeOfTelephone="business"), - ), +def _header(identity: DemoIdentity) -> DppHeader: + return DppHeader( + digital_product_passport_id=str(identity.dpp_id), + unique_product_identifier=identity.product_id, + granularity=Granularity.MODEL, + dpp_schema_version="1.0", + dpp_status="active", + last_updated=datetime(2026, 1, 1, tzinfo=UTC), + economic_operator_id=f"operator-{identity.run_id.hex}", ) def build_minimal_fixture(identity: DemoIdentity) -> Dpp4Fun: - """Build the smallest structurally and semantically valid furniture DPP.""" + """Build the smallest current typed furniture passport for the demo.""" - return Dpp4Fun( - coreDpp=DppCore( - passportMetadata=PassportMetadata( - uniqueProductIdentifier=identity.dpp_id, - passportUpdateDates=(date(2024, 1, 1),), - ), - nameplate=Nameplate( - gtinCode=identity.product_id, - manufacturer=_manufacturer(complete=False), - ), - ), - classification=ProductClassification(sector="Furniture", category="Office Chair"), - characteristics=Characteristics( - productName="CIR4FUN Demo Chair", - productType="Office Chair", - ), + return Dpp4Fun.create( + _header(identity), + Nameplate.create(manufacturerName="CIR4FUN Demo Manufacturer"), + ChairCharacteristics.create(color="blue", surface="Smooth"), ) def build_complete_fixture(identity: DemoIdentity) -> Dpp4Fun: - """Build a deterministic fixture with every optional model group populated.""" - - return Dpp4Fun( - coreDpp=DppCore( - passportMetadata=PassportMetadata( - uniqueProductIdentifier=identity.dpp_id, - passportUpdateDates=(date(2024, 1, 1), date(2024, 6, 1)), - qrCodeOrDigitalTag=identity.registry_sensitive_id, - externalDocumentationLink="https://example.invalid/dpp/demo-chair", + """Build all 19 general aspects plus typed repeated, multilingual, unit, and record data.""" + + materials = ( + Material.create(name="Steel", portion=0.6, mandatory=True), + Material.create(name="Foam", portion=0.4), + ) + characteristic = ChairCharacteristics.create( + color="blue", + surface="Woven matte", + seatingCapacity=1, + swivel=True, + ).with_quantity("height", Dpp4FunQuantity(value=0.82, unit=LengthUnit.METER)) + return Dpp4Fun.create( + _header(identity), + Nameplate.create( + gtinCode=identity.product_id, + manufacturerName="CIR4FUN Demo Manufacturer", + manufacturerProductDesignation={ + "en-US": "Demo Office Chair", + "de-DE": "Demo-Bürostuhl", + }, + ), + BillOfMaterials.create(materialSet=tuple(item.element for item in materials)), + CarbonFootprint.create( + carbonFootprintCalculationSet=( + CarbonFootprintCalculation.create(co2eq=18.5, phase="cradle-to-gate").element, + ) + ), + Durability.create( + cycleLife="10,000 cycles", + standardSet=( + StandardAssessment.create( + name="CIR4FUN durability assessment", + description="Repeat cycle assessment for the demo chair.", + complianceStatus="compliant", + ).element, ), - nameplate=Nameplate( - gtinCode=identity.product_id, - internalArticleNumber="C4F-DEMO-CHAIR", - batchNumber="DEMO-BATCH-2024", - customsTariffNumber="94013000", - uriOfTheProduct="https://example.invalid/products/demo-chair", - manufacturer=_manufacturer(complete=True), - supplier=Organization( - name="CIR4FUN Demo Supplier", - gln="4012345000016", - role=OrganizationRole.SUPPLIER, + ), + EnvironmentalImpact.create( + indicatorSet=( + _profile_collection( + "environmentalImpact.indicatorSet[*]", + name="Climate impact", + description="Demonstration environmental indicator.", + value="18.5", + unit="kg CO2e", ), - ), - documentation=Documentation( - digitalInstructionsLink="https://example.invalid/docs/demo-chair", - safetyInstructionsLink="https://example.invalid/docs/demo-chair-safety", - downloadable=True, - availableForYears=10, - paperCopyAvailableOnRequest=True, + ) + ), + HealthImpact.create( + indicatorSet=( + HealthIndicator.create( + name="Indoor emissions", + description="Demonstration health indicator.", + value="low", + unit="mg/m3", + ).element, + ) + ), + ProductRelationship.create( + accessorySet=( + Accessory.create( + name="Replacement cushion", + description="Optional replacement cushion.", + reference="https://example.test/accessories/cushion", + ).element, + ) + ), + Recyclability.create(recycledContent=0.35, recyclable=True), + Refurbishment.create( + refurbishmentInstructionSet=( + _profile_collection( + "refurbishment.refurbishmentInstructionSet[*]", + name="Refinish the frame", + description="Clean and refinish the chair frame.", + reference="https://example.test/instructions/refinish", + ), + ) + ), + Reliability.create( + certificateSet=( + Certificate.create( + name="CIR4FUN reliability assessment", + description="Demonstration reliability evidence.", + reference="https://example.test/certificates/reliability", + ).element, + ) + ), + Remanufacturing.create( + assemblyInstructionSet=( + _profile_collection( + "remanufacturing.assemblyInstructionSet[*]", + name="Reassemble the frame", + description="Reassemble the refurbished chair frame.", + targetAudience="professional", + reference="https://example.test/instructions/reassemble", + ), + ) + ), + Repairability.create( + sparePartsAvailable=True, + repairRecordSet=( + RepairRecord.create( + name="Demo repair record", + description="A completed example repair.", + operator=f"operator-{identity.run_id.hex}", + time="2026-01-01T00:00:00Z", + ).element, ), ), - classification=ProductClassification( - sector="Furniture", - group="Seating", - category="Office Chair", - subCategory="Ergonomic Office Chair", - tags=("ergonomic", "adjustable"), + Reusability.create(secondHand=True), + RightOfEndUsers.create( + postSaleServiceSet=( + PostSaleService.create( + name="Product support", + description="Contact the operator for product support.", + reference="https://example.test/support", + ).element, + ) ), - characteristics=Characteristics( - productName="CIR4FUN Demo Chair", - description="Deterministic complete SDK demonstration fixture", - brand="CIR4FUN", - productType="Office Chair", - dimensions=Dimensions(width=60.0, height=120.0, depth=60.0, unit="cm"), - weight=14.5, - color="blue", - features=("lumbar-support", "height-adjustable"), + Safety.create(ageRestriction="adult", professionalUseOnly=False), + Traceability.create( + currentPhase="final-assembly", + designRecordSet=( + DesignRecord.create( + name="Chair design record", + description="Approved chair design record.", + operator=f"operator-{identity.run_id.hex}", + time="2026-01-01T00:00:00Z", + status="approved", + ).element, + ), ), - billOfMaterials=BillOfMaterials( - materials=( - Material(name="Steel", mandatory=True, portion=0.6, reference="MAT-STEEL"), - Material(name="Foam", portion=0.4, reference="MAT-FOAM"), + Upgradability.create(upgradeOptionsAvailable=True), + Usage.create( + operatingHours=2000, + assemblyInstructionSet=( + Instruction.create( + name="Assemble the chair", + description="Attach the seat and backrest to the frame.", + reference="https://example.test/instructions/assembly", + ).element, ), - components=(Component(name="Seat assembly", reference="COMP-SEAT"),), - parts=(Part(name="Fastener set", mandatory=True, reference="PART-FASTENER"),), ), + WasteManagement.create(toxicityRisk=False), + characteristic, ) diff --git a/examples/mock-services-demo/src/dpp_mock_services_demo/integration_scenarios.py b/examples/mock-services-demo/src/dpp_mock_services_demo/integration_scenarios.py index c502ab8..3a84015 100644 --- a/examples/mock-services-demo/src/dpp_mock_services_demo/integration_scenarios.py +++ b/examples/mock-services-demo/src/dpp_mock_services_demo/integration_scenarios.py @@ -1,31 +1,27 @@ -"""Annotated connected walkthrough using public SDK clients and mock-service HTTP APIs. - -The normal ``demo`` command selects the compact lifecycle in ``_PROFILE_STEP_IDS``: -prepare a DPP, create/read/update it through the repository, register it, and -remove the record it created. ``integration`` retains the longer educational -path for compatibility and maintainer-oriented investigation. -""" +"""Connected example using typed models at the edge and generic JSON clients.""" from __future__ import annotations +import json from dataclasses import asdict, dataclass, replace -from datetime import UTC, datetime from time import perf_counter -from typing import Any +from typing import Any, Protocol import dpp_sdk from dpp_sdk import Dpp4FunJsonCodec, validate_dpp4fun -from dpp_sdk.clients import DppHttpClientError, DppRegistryClient, DppRepoClient, RegisterDppRequest +from dpp_sdk.clients import ( + DppApiClientError, + DppApiResponse, + DppRegistryClient, + DppRepoClient, + DppRepresentation, + RegisterDppRequest, +) from .config import DemoConfig from .fixtures import DemoIdentity, build_complete_fixture from .image_identity import RuntimeImageIdentity - -_INTEGRATION_STATUSES = frozenset({"PASS", "EXPECTED_ERROR", "BLOCKED", "SKIP", "FAIL"}) -_PROFILE_STEP_IDS = { - "demo": frozenset({1, 2, 3, 4, 5, 8, 12, 14}), - "integration": frozenset(range(1, 15)), -} +from .readiness import wait_for_health @dataclass(frozen=True) @@ -47,6 +43,18 @@ class IntegrationStep: duration_seconds: float +@dataclass(frozen=True) +class IntegrationRunContext: + run_id: object + dpp_id: object + product_id: str + initial_dpp: object + repository_created: bool = False + repository_cleanup: str = "not_created" + post_delete_result: str = "not_attempted" + registration_id: str = "" + + @dataclass(frozen=True) class IntegrationReport: identity: DemoIdentity @@ -59,621 +67,353 @@ class IntegrationReport: profile: str = "integration" -@dataclass(frozen=True) -class IntegrationRunContext: - """One generated lifecycle and the resources it owns.""" +class RepositoryClient(Protocol): + def create_dpp(self, dpp: object, representation: DppRepresentation = ...) -> object: ... + + def read_dpp_by_id( + self, dpp_id: str, representation: DppRepresentation = ... + ) -> DppApiResponse: ... + + def delete_dpp_by_id(self, dpp_id: str) -> object: ... + + def close(self) -> None: ... - run_id: object - dpp_id: object - product_id: str - initial_dpp: object - updated_dpp: object | None = None - registration_id: str = "" - repository_created: bool = False - repository_cleanup: str = "not_created" - post_delete_result: str = "not_attempted" - registry_state: str = "not_attempted" + +class RegistryClient(Protocol): + def post_new_dpp_to_registry(self, request: RegisterDppRequest) -> object: ... + + def close(self) -> None: ... def _step( - step_id: str, + index: int, stage: str, title: str, purpose: str, operation: str, service: dict[str, str], + action: Any, *, - input: dict[str, object] | None = None, - response: dict[str, object] | None = None, - proof: dict[str, object] | None = None, - explanation: str = "", - why: str = "", - status: str = "PASS", + input: dict[str, object], cleanup: str = "none", - duration: float = 0.0, -) -> IntegrationStep: - if status not in _INTEGRATION_STATUSES: - raise ValueError(f"unsupported integration status: {status}") - if service: - service = { - **service, - "interaction": f"{service['http_method']} {service['route']}", - } - elif status == "BLOCKED": - service = { - "name": "not contacted", - "interaction": "No service request was attempted after the failed prerequisite.", - } - else: - service = { - "name": "local SDK", - "interaction": "No external service call; this step runs in the Python SDK process.", - } - if response is None: - if status == "BLOCKED": - response = {"result": "not attempted"} - else: - response = {"result": "not applicable"} - if proof is None: - proof = ( - {"blocked_by": "an earlier failed prerequisite"} - if status == "BLOCKED" - else {"proof": "not applicable"} - ) - return IntegrationStep( - step_id, - stage, - title, - status, - "SDK_LOCAL" if stage == "local" else "LIVE_MOCK_IMAGE", - purpose, - input or {"selected": "not applicable"}, - {"public_api": operation, "display": operation}, - service, +) -> tuple[IntegrationStep, Any]: + started = perf_counter() + try: + response, proof = action() + status = "PASS" + except Exception as exc: # noqa: BLE001 - preserve each operation failure in report + response = {"error": f"{type(exc).__name__}: {exc}"} + proof = {"proof": "operation failed"} + status = "FAIL" + interaction = f"{service['http_method']} {service['route']}" if service else "No HTTP request" + return ( + IntegrationStep( + f"INT-{index:02d}", + stage, + title, + status, + "LIVE_MOCK_IMAGE" if service else "SDK_LOCAL", + purpose, + input, + {"public_api": operation, "display": operation}, + {**service, "interaction": interaction}, + response if isinstance(response, dict) else {"result": response}, + proof if isinstance(proof, dict) else {"proof": proof}, + "The report records the JSON exchanged with the selected public endpoint.", + "Applications can keep typed domain objects while clients remain JSON based.", + cleanup, + perf_counter() - started, + ), response, - proof, - explanation or "The displayed result was captured from this step.", - why or "It makes the live workflow explicit and repeatable.", - cleanup, - duration, ) -def _ready_response(ready: object, endpoint: str) -> tuple[dict[str, object], dict[str, object]]: - """Require a functional public health response before the connected flow proceeds.""" - - if ready is False: - raise RuntimeError(f"health_check returned False for {endpoint}") - if ready is not True: - raise RuntimeError(f"health_check must return True for {endpoint}, got {ready!r}") - return {"ready": True}, {"endpoint": endpoint} +def _blocked_steps() -> tuple[IntegrationStep, ...]: + titles = ( + ("Repository readiness", "GET", "/health"), + ("Registry readiness", "GET", "/health"), + ("Validate typed DPP", "", ""), + ("Create DPP", "POST", "/v1/dpps"), + ("Read DPP JSON", "GET", "/v1/dpps/{digitalProductPassportId}"), + ("Register DPP", "POST", "/v1/registerDPP"), + ("Demonstrate not found mapping", "GET", "/v1/dpps/{unknownId}"), + ("Delete created DPP", "DELETE", "/v1/dpps/{digitalProductPassportId}"), + ) + return tuple( + IntegrationStep( + f"INT-{index:02d}", + "blocked", + title, + "BLOCKED", + "NOT_RUN", + "Select a service profile before running the connected flow.", + {"selected": "not available"}, + {"public_api": "not invoked", "display": "not invoked"}, + {"name": "not contacted", "interaction": "No service request was attempted."}, + {"result": "not attempted"}, + {"blocked_by": "missing service configuration"}, + "No live configuration was supplied.", + "The demo does not guess service endpoints.", + "not_created", + 0.0, + ) + for index, (title, method, route) in enumerate(titles, 1) + ) def run_integration_scenarios( identity: DemoIdentity, config: DemoConfig | None, - repository: Any = None, - registry: Any = None, + repository: RepositoryClient | None = None, + registry: RegistryClient | None = None, profile: str = "integration", image_identity: RuntimeImageIdentity | None = None, image_identity_error: str = "", ) -> IntegrationReport: - """Run one DPP lifecycle; tests may pass ``None`` config to inspect blocked shape. - - The flow reports each completed operation as evidence instead of printing - directly. This lets the CLI render a readable walkthrough and lets tests - inspect the same structured result. Records are created with a generated - identity and deleted in ``finally`` so cleanup remains scoped to this run. - """ - if profile not in _PROFILE_STEP_IDS: + """Run one disposable JSON API lifecycle and report cleanup explicitly.""" + if profile not in {"demo", "integration"}: raise ValueError(f"unsupported integration profile: {profile}") - selected_steps = _PROFILE_STEP_IDS[profile] fixture = build_complete_fixture(identity) - context = IntegrationRunContext( - run_id=identity.run_id, - dpp_id=identity.dpp_id, - product_id=identity.product_id, - initial_dpp=fixture, - ) + context = IntegrationRunContext(identity.run_id, identity.dpp_id, identity.product_id, fixture) if config is None: - steps = tuple( - _step( - f"INT-{i:02d}", - "environment", - "Configuration required", - "Use an explicit service profile.", - "DppRepoClient", - {}, - status="BLOCKED", - explanation="No live profile was supplied.", - why="The demo never guesses an endpoint.", - ) - for i in sorted(selected_steps) - ) return IntegrationReport( - identity, None, steps, context, image_identity, image_identity_error, profile=profile + identity, + None, + _blocked_steps(), + context, + image_identity, + image_identity_error, + profile=profile, ) - # Accept injected clients for tests, while normal CLI runs construct only - # public SDK clients from the selected service profile. - repo = repository or DppRepoClient(config.repo_base_url, Dpp4FunJsonCodec(), validate_dpp4fun) + + repo_owned, registry_owned = repository is None, registry is None + repo = repository or DppRepoClient(config.repo_base_url) reg = registry or DppRegistryClient(config.registry_base_url) - owned_repo, owned_reg, active, warnings = repository is None, registry is None, False, [] + codec = Dpp4FunJsonCodec() + wire = json.loads(codec.to_json(fixture)) steps: list[IntegrationStep] = [] - created_at: datetime | None = None - updated_at: datetime | None = None - - def run( - i: int, - stage: str, - title: str, - purpose: str, - operation: str, - service: dict[str, str], - action: Any, - **kwargs: Any, - ) -> Any: - start = perf_counter() - try: - response, proof = action() - steps.append( - _step( - f"INT-{i:02d}", - stage, - title, - purpose, - operation, - service, - response=response, - proof=proof, - duration=perf_counter() - start, - **kwargs, - ) - ) - return response - except Exception as exc: - steps.append( - _step( - f"INT-{i:02d}", - stage, - title, - purpose, - operation, - service, - status="FAIL", - response={"error": f"{type(exc).__name__}: {exc}"}, - duration=perf_counter() - start, - **kwargs, - ) - ) - raise + warnings: list[str] = [] + active = False + context = replace(context) try: - # Each nested action returns response plus persistence proof; ``run`` - # converts that into one consistently rendered teaching step. - - def create() -> tuple[dict[str, object], dict[str, object]]: - nonlocal created_at, context - response = repo.create_dpp(fixture) - created_at = datetime.now(UTC) - context = replace(context, repository_created=True, repository_cleanup="created") - return {"dppId": str(response.dppId)}, {"submitted_dpp_id": str(identity.dpp_id)} - - def full_update() -> tuple[dict[str, object], dict[str, object]]: - nonlocal context, updated_at - updated = repo.update_dpp_by_id( - str(identity.dpp_id), {"characteristics": {"productName": changed}} - ) - context = replace(context, updated_dpp=updated) - updated_at = datetime.now(UTC) - return ( - {"returned_productName": updated.productName}, - { - "read_back_productName": repo.read_dpp_by_id(str(identity.dpp_id)).productName, - "original_local_productName": before, - }, - ) - - def bulk_lookup() -> tuple[dict[str, object], dict[str, object]]: - identifiers = [ - str(value) - for value in repo.read_dpp_ids_by_product_ids( - [identity.product_id], limit=10, cursor="0" - ).dppIdentifiers - ] - includes_created = str(identity.dpp_id) in identifiers - if not includes_created: - raise AssertionError("bulk lookup did not include the created DPP") - return {"dpp_ids": identifiers}, {"includes_created_dpp": includes_created} - - def read_by_dpp_id() -> tuple[dict[str, object], dict[str, object]]: - read_back = repo.read_dpp_by_id(str(identity.dpp_id)) - matches_created = str(read_back.dpp_id) == str(identity.dpp_id) - if not matches_created: - raise AssertionError("read-back DPP ID did not match the created DPP") - return ( - {"dppId": str(read_back.dpp_id), "productName": read_back.productName}, - {"read_back_dpp_id_matches_created": matches_created}, - ) - - def read_by_product_id() -> tuple[dict[str, object], dict[str, object]]: - resolved = repo.read_dpp_by_product_id(identity.product_id) - matches_created = str(resolved.dpp_id) == str(identity.dpp_id) - if not matches_created: - raise AssertionError("product-ID lookup did not resolve the created DPP") - return ( - {"dppId": str(resolved.dpp_id), "productId": identity.product_id}, - {"resolved_dpp_id_matches_created": matches_created}, - ) - - run( - 1, - "environment", - "Repository readiness", - "Confirm the repository is reachable.", - "DppRepoClient.health_check()", - {"name": "repository", "http_method": "GET", "route": "/health"}, - lambda: _ready_response(repo.health_check(), config.repo_base_url), - input={"endpoint": config.repo_base_url}, - explanation="The repository answered its public health endpoint.", - why="Business requests should follow a functional readiness check.", + probes = ( + ("repository", config.repo_base_url), + ("registry", config.registry_base_url), ) - run( - 2, - "environment", - "Registry readiness", - "Confirm the registry is reachable.", - "DppRegistryClient.health_check()", - {"name": "registry", "http_method": "GET", "route": "/health"}, - lambda: _ready_response(reg.health_check(), config.registry_base_url), - input={"endpoint": config.registry_base_url}, - explanation="The registry answered its public health endpoint.", - why="Registration needs an available registry service.", - ) - run( - 3, - "local", - "Build and validate a DPP", - "Construct and validate the typed model before I/O.", - "validate_dpp4fun(dpp)", - {}, - lambda: ( - validate_dpp4fun(fixture) - or ( - {"model": "Dpp4Fun"}, + for index, (name, endpoint) in enumerate(probes, 1): + readiness = wait_for_health( + scenario_id=f"INT-{index:02d}", + name=f"{name.title()} readiness", + endpoint=endpoint, + timeout_seconds=config.startup_timeout_seconds, + ) + steps.append( + IntegrationStep( + f"INT-{index:02d}", + "environment", + f"{name.title()} readiness", + "PASS" if readiness.status.value == "PASSED" else "FAIL", + "LIVE_MOCK_IMAGE", + f"Confirm {name} is reachable before making requests.", + {"endpoint": endpoint}, + {"public_api": "GET /health", "display": "http.get('/health')"}, { - "productName": fixture.productName, - "serialized_preview": Dpp4FunJsonCodec().to_json(fixture)[:160], + "name": name, + "http_method": "GET", + "route": "/health", + "interaction": f"GET {endpoint.rstrip('/')}/health", }, + {"details": readiness.details}, + {"ready": readiness.status.value == "PASSED"}, + "Readiness uses a direct HTTP health probe.", + "The clients expose API operations only.", + "none", + readiness.duration_seconds, ) - ), - input={ - "dppId": str(identity.dpp_id), - "productId": identity.product_id, - "productName": fixture.productName, - }, - explanation="This stage is local only.", - why="Typed validation catches invalid complete documents before creation.", - ) - run( - 4, - "create", - "Create the DPP", - "Persist the validated DPP.", - "DppRepoClient.create_dpp(dpp)", - {"name": "repository", "http_method": "POST", "route": "/v1/dpps"}, - create, - input={"dppId": str(identity.dpp_id), "productName": fixture.productName}, - explanation="The SDK serializes the typed DPP and decodes the response.", - why="Applications use typed values while the Mock service owns storage.", - ) - active = True - run( - 5, - "read", - "Read by DPP ID", - "Read the stored typed DPP.", - "DppRepoClient.read_dpp_by_id(dpp_id)", - { - "name": "repository", - "http_method": "GET", - "route": "/v1/dpps/{dppId}?representation=full", - }, - read_by_dpp_id, - input={"dppId": str(identity.dpp_id)}, - explanation="The full response is decoded through the public codec.", - why="Read-back proves persistence.", - ) - if 6 in selected_steps: - run( - 6, - "read", - "Read by product ID", - "Resolve the DPP through its product identifier.", - "DppRepoClient.read_dpp_by_product_id(product_id)", - { - "name": "repository", - "http_method": "GET", - "route": "/v1/dppsByProductId/{productId}?representation=full", - }, - read_by_product_id, - input={"productId": identity.product_id}, - explanation="The repository supports both public identifiers.", - why="Consumers need identifier-based access without raw HTTP.", - ) - if 7 in selected_steps: - run( - 7, - "read", - "Bulk lookup by product ID", - "Find generated DPP identifiers through the public bulk lookup operation.", - "DppRepoClient.read_dpp_ids_by_product_ids(product_ids, limit, cursor)", - {"name": "repository", "http_method": "POST", "route": "/v1/dppsByProductIds"}, - bulk_lookup, - input={"product_ids": [identity.product_id], "limit": 10, "cursor": "0"}, - explanation="The SDK constructs the bulk request and decodes the identifier page.", - why="Consumers can locate known product identifiers without one request per DPP.", - ) - before = fixture.productName - changed = f"CIR4FUN Demo Chair updated {identity.run_id.hex[:8]}" - run( - 8, - "update", - "Update and read back", - "Apply a bounded merge patch and prove it persisted.", - "DppRepoClient.update_dpp_by_id(dpp_id, patch)", - {"name": "repository", "http_method": "PATCH", "route": "/v1/dpps/{dppId}"}, - full_update, - input={"before": before, "patch": {"characteristics": {"productName": changed}}}, - explanation="The server validates the resulting complete DPP.", - why="Partial updates avoid resending unrelated fields.", - ) - current_lookup_at = datetime.now(UTC) - - def history() -> tuple[dict[str, object], dict[str, object]]: - historical = repo.read_dpp_version_by_id_and_date(str(identity.dpp_id), created_at) - updated_historical = repo.read_dpp_version_by_id_and_date( - str(identity.dpp_id), updated_at - ) - current = repo.read_dpp_version_by_id_and_date(str(identity.dpp_id), current_lookup_at) - if historical.productName != fixture.productName: - raise AssertionError("historical lookup did not return the created version") - if updated_historical.productName != changed: - raise AssertionError( - "updated historical lookup did not return the full-patch version" - ) - if current.productName != changed: - raise AssertionError("current history lookup did not return the updated version") - return ( - { - "historical_productName": historical.productName, - "updated_historical_productName": updated_historical.productName, - "current_productName": current.productName, - }, - { - "created_historical_differs_from_current": historical.productName - != current.productName, - "updated_historical_matches_current": updated_historical.productName - == current.productName, - }, ) - - def fine_update() -> tuple[dict[str, object], dict[str, object]]: - nonlocal context - updated_value = repo.update_data_element( - str(identity.dpp_id), "$.characteristics.productName", fine_changed - ) - updated = repo.read_dpp_by_id(str(identity.dpp_id)) - context = replace(context, updated_dpp=updated) - return ( - {"updated_value": updated_value}, - { - "read_back_value": repo.read_data_element( - str(identity.dpp_id), "$.characteristics.productName" - ), - "full_dpp_value": updated.productName, - }, + if readiness.status.value != "PASSED": + steps.extend(_blocked_steps()[index:7]) + break + + if len(steps) == 2: + result = validate_dpp4fun(fixture) + step, _ = _step( + 3, + "local", + "Validate typed DPP", + "Check the profile at the application boundary.", + "validate_dpp4fun(dpp)", + {}, + lambda: ( + ( + {"valid": result.is_valid, "issues": len(result.issues)}, + {"valid": result.is_valid}, + ) + if result.is_valid + else (_ for _ in ()).throw( + ValueError(f"DPP4Fun validation failed: {result.issues!r}") + ) + ), + input={"digitalProductPassportId": str(identity.dpp_id)}, ) - - def fine_read() -> tuple[dict[str, object], dict[str, object]]: - value = repo.read_data_element(str(identity.dpp_id), "$.characteristics.productName") - matches_full_patch = value == changed - if not matches_full_patch: - raise AssertionError("fine-grained read did not match the full update") - return ({"value": value}, {"read_value_matches_full_patch": matches_full_patch}) - - if 9 in selected_steps: - if created_at is None or updated_at is None: - raise AssertionError("create/update timestamps were not captured") - run( - 9, - "history", - "Read history", - "Compare original and current persisted versions.", - "DppRepoClient.read_dpp_version_by_id_and_date(dpp_id, date)", + steps.append(step) + + def create() -> tuple[dict[str, object], dict[str, object]]: + nonlocal active, context + response = repo.create_dpp(wire, DppRepresentation.FULL) + dpp_id = response.digital_product_passport_id + if dpp_id != str(identity.dpp_id): + raise AssertionError(f"unexpected created DPP identifier {dpp_id!r}") + active = True + context = replace(context, repository_created=True) + return {"digitalProductPassportId": dpp_id}, {"identifier_matches": True} + + step, _ = _step( + 4, + "create", + "Create DPP", + "Send canonical DPP JSON to the repository.", + "DppRepoClient.create_dpp(json_value, FULL)", { "name": "repository", - "http_method": "GET", - "route": "/v1/dppsByIdAndDate/{dppId}", - }, - history, - input={ - "dppId": str(identity.dpp_id), - "created_at_utc": created_at.isoformat(), - "updated_at_utc": updated_at.isoformat(), - "current_lookup_at_utc": current_lookup_at.isoformat(), + "http_method": "POST", + "route": "/v1/dpps?representation=full", }, - explanation="The public history operation takes an aware UTC instant.", - why="Consumers can resolve an earlier persisted representation.", + create, + input={"digitalProductPassportId": str(identity.dpp_id)}, + cleanup="deleted at end of run", ) - if 10 in selected_steps: - run( - 10, - "fine_grained", - "Read one element", - "Read the selected product name without fetching a full DPP.", - "DppRepoClient.read_data_element(dpp_id, element_path)", + steps.append(step) + + def read() -> tuple[dict[str, object], dict[str, object]]: + response = repo.read_dpp_by_id(str(identity.dpp_id), DppRepresentation.FULL) + if response.payload != wire: + raise AssertionError("FULL JSON response differs from submitted document") + return { + "digitalProductPassportId": response.payload.get("digitalProductPassportId") + }, {"matches_submitted_json": True} + + step, _ = _step( + 5, + "read", + "Read DPP JSON", + "Read the created DPP as canonical JSON.", + "DppRepoClient.read_dpp_by_id(id, FULL)", { "name": "repository", "http_method": "GET", - "route": "/v1/dpps/{dppId}/elements/{elementPath}", + "route": "/v1/dpps/{id}?representation=full", }, - fine_read, - input={"selector": "$.characteristics.productName"}, - explanation="The SDK encodes the bounded selector as one path segment.", - why="Fine-grained access can reduce response handling.", + read, + input={"digitalProductPassportId": str(identity.dpp_id)}, + cleanup="deleted at end of run", ) - fine_changed = f"{changed} fine" - if 11 in selected_steps: - run( - 11, - "fine_grained", - "Update one element and read it back", - "Update the selected product name without replacing the full DPP.", - "DppRepoClient.update_data_element(dpp_id, element_path, value)", - { - "name": "repository", - "http_method": "PATCH", - "route": "/v1/dpps/{dppId}/elements/{elementPath}", - }, - fine_update, - input={"selector": "$.characteristics.productName", "value": fine_changed}, - explanation=( - "The SDK sends the selected element as an encoded dynamic path segment." - ), - why=( - "Consumers can make a narrow, traceable change when the public service " - "supports it." - ), - ) - request = RegisterDppRequest( - uniqueProductIdentifier=identity.product_id, - digitalProductPassportId=str(identity.dpp_id), - uniqueEconomicOperatorIdentifier=identity.registry_sensitive_id, - dppApiEndpoint=config.repo_base_url, - ) + steps.append(step) - def register() -> tuple[dict[str, object], dict[str, object]]: - nonlocal context - registration = reg.post_new_dpp_to_registry(request) - context = replace( - context, - registration_id=registration.registrationId, - registry_state="registered_retained_no_public_cleanup", - ) - return ( - {"registrationId": registration.registrationId}, - {"registry_read_back": "not supported by public SDK"}, + request = RegisterDppRequest( + unique_product_identifier=identity.product_id, + digital_product_passport_id=str(identity.dpp_id), + unique_economic_operator_identifier=identity.registry_sensitive_id, + dpp_api_endpoint=f"{config.repo_base_url.rstrip('/')}/v1/dpps", ) - if 12 in selected_steps: - run( - 12, + def register() -> tuple[dict[str, object], dict[str, object]]: + nonlocal context + response = reg.post_new_dpp_to_registry(request) + registration_id = response.registration_id + if not registration_id: + raise AssertionError("registrationId missing from response") + context_update = replace(context, registration_id=registration_id) + # Assigning the frozen context is local report state, not SDK state. + context = context_update + return {"registrationId": registration_id}, { + "request_fields": list(request.model_dump(by_alias=True)) + } + + step, _ = _step( + 6, "registry", - "Register the DPP", - "Register the repository-backed DPP.", + "Register DPP", + "Register endpoint metadata as JSON.", "DppRegistryClient.post_new_dpp_to_registry(request)", {"name": "registry", "http_method": "POST", "route": "/v1/registerDPP"}, register, - input=request.model_dump(), - explanation="The registry verifies the referenced repository DPP.", - why="Registration connects public DPP metadata to a repository endpoint.", + input={"digitalProductPassportId": str(identity.dpp_id)}, + cleanup="registry lifecycle is managed by its service", ) + steps.append(step) - def invalid() -> tuple[dict[str, object], dict[str, object]]: - old = repo.read_dpp_by_id(str(identity.dpp_id)).productName - try: - repo.update_dpp_by_id( - str(identity.dpp_id), {"passportMetadata": {"uniqueProductIdentifier": "bad"}} - ) - except DppHttpClientError as exc: - return {"exception": "DppHttpClientError", "http_status": exc.status_code}, { - "unchanged": repo.read_dpp_by_id(str(identity.dpp_id)).productName == old - } - raise AssertionError("invalid patch accepted") - - if 13 in selected_steps: - response, proof = invalid() - steps.append( - _step( - "INT-13", - "error", - "Reject invalid patch atomically", - "Show a rejected patch does not change storage.", - "DppRepoClient.update_dpp_by_id(dpp_id, patch)", - {"name": "repository", "http_method": "PATCH", "route": "/v1/dpps/{dppId}"}, - input={"patch": {"passportMetadata": {"uniqueProductIdentifier": "bad"}}}, - response=response, - proof=proof, - status="EXPECTED_ERROR", - explanation="The service rejected an immutable identifier change.", - why="Atomic rejection prevents partial corrupted state.", - ) + def not_found() -> tuple[dict[str, object], dict[str, object]]: + try: + repo.read_dpp_by_id("missing-" + identity.run_id.hex) + except DppApiClientError as exc: + return {"exception": type(exc).__name__, "http_status": exc.http_status}, { + "api_status": exc.status_code.value if exc.status_code else None + } + raise AssertionError("unknown DPP did not produce API error") + + step, _ = _step( + 7, + "error", + "Demonstrate not-found mapping", + "Show structured API error mapping.", + "DppRepoClient.read_dpp_by_id(unknown_id)", + {"name": "repository", "http_method": "GET", "route": "/v1/dpps/{unknownId}"}, + not_found, + input={"digitalProductPassportId": "unknown"}, ) - except Exception: # noqa: BLE001 - the report records the failed step and blocks dependents - pass + if step.status == "PASS": + step = replace(step, status="EXPECTED_ERROR") + steps.append(step) finally: - # Only delete the generated repository record after a successful create. - # Registry cleanup is intentionally not attempted: its public SDK API - # does not expose a matching deletion operation. if active: try: repo.delete_dpp_by_id(str(identity.dpp_id)) - active = False - try: - repo.read_dpp_by_id(str(identity.dpp_id)) - raise AssertionError("deleted DPP remained readable") - except DppHttpClientError as exc: - if exc.status_code != 404: - raise - steps.append( - _step( - "INT-14", - "cleanup", - "Delete the demonstration DPP", - "Remove the repository record created for this run.", - "DppRepoClient.delete_dpp_by_id(dpp_id)", - { - "name": "repository", - "http_method": "DELETE", - "route": "/v1/dpps/{dppId}", - }, - input={"dppId": str(identity.dpp_id)}, - response={"deleted": True}, - proof={"post_delete_read": "HTTP 404"}, - explanation="Registry cleanup is unavailable through the public SDK.", - why="A generated identity makes cleanup safe and scoped.", - cleanup="repository deleted; registry cleanup not supported", - ) - ) context = replace( - context, - repository_cleanup="deleted", - post_delete_result="HTTP 404", + context, repository_cleanup="deleted", post_delete_result="DELETE succeeded" ) - except Exception as exc: - warnings.append(f"repository cleanup failed: {type(exc).__name__}: {exc}") + except Exception as exc: # noqa: BLE001 - cleanup errors are report evidence context = replace(context, repository_cleanup="delete_failed") - if owned_reg: - reg.close() - if owned_repo: + warnings.append(f"repository cleanup failed: {type(exc).__name__}: {exc}") + if repo_owned: repo.close() - for i in sorted(selected_steps): - if any(step.step_id == f"INT-{i:02d}" for step in steps): - continue - steps.append( - _step( - f"INT-{i:02d}", - "blocked", - "Prerequisite failed", - "A prior live step failed.", - "not executed", - {}, - status="BLOCKED", - explanation="The connected flow stopped safely.", - why="Dependent operations must not be reported as passed.", - ) + if registry_owned: + reg.close() + + steps.append( + IntegrationStep( + step_id="INT-08", + stage="cleanup", + title="Delete created DPP", + status=( + "PASS" + if context.repository_cleanup == "deleted" + else "FAIL" + if context.repository_cleanup == "delete_failed" + else "SKIP" + ), + evidence_class="LIVE_MOCK_IMAGE", + purpose="Remove the disposable record created by this run.", + input={"digitalProductPassportId": str(identity.dpp_id)}, + operation={ + "public_api": "DppRepoClient.delete_dpp_by_id(id)", + "display": "repo.delete_dpp_by_id(id)", + }, + service={ + "name": "repository", + "http_method": "DELETE", + "route": "/v1/dpps/{id}", + "interaction": "DELETE /v1/dpps/{digitalProductPassportId}", + }, + response={"repository": context.repository_cleanup}, + persistence_proof={"public_sdk_deleted": context.repository_cleanup == "deleted"}, + explanation="Cleanup runs for records created by this invocation.", + why_it_matters="Disposable test records should not accumulate.", + cleanup_effect=context.repository_cleanup, + duration_seconds=0.0, ) - steps.sort(key=lambda step: step.step_id) + ) return IntegrationReport( identity, config, @@ -688,159 +428,99 @@ def invalid() -> tuple[dict[str, object], dict[str, object]]: def integration_payload(report: IntegrationReport) -> dict[str, object]: steps = [asdict(step) for step in report.steps] - counts = { - key: sum(step["status"] == key for step in steps) - for key in ("PASS", "EXPECTED_ERROR", "BLOCKED", "SKIP", "FAIL") + statuses = [step.status for step in report.steps] + summary = { + "total": len(statuses), + "passed": statuses.count("PASS"), + "expected_error": statuses.count("EXPECTED_ERROR"), + "blocked": statuses.count("BLOCKED"), + "skipped": statuses.count("SKIP"), + "fail": statuses.count("FAIL"), + "steps_completed": statuses.count("PASS"), + "expected_errors_demonstrated": statuses.count("EXPECTED_ERROR"), + "blocked_steps": statuses.count("BLOCKED"), + "skipped_steps": statuses.count("SKIP"), + "unexpected_failures": statuses.count("FAIL"), } + config = report.config + runtime = report.image_identity return { - "schema_version": 1, - "report_type": "live_demonstration" - if report.profile == "demo" - else "live_integration_demonstration", - "mode": report.profile, - "run_id": str(report.identity.run_id), - "sdk": {"version": dpp_sdk.__version__, "location": str(dpp_sdk.__file__)}, + "profile": report.profile, + "sdk": {"version": dpp_sdk.__version__}, "services": { "repository": { - "endpoint": report.config.repo_base_url if report.config else "", - "configured_image": report.config.repo_image if report.config else "", - "runtime_digest": ( - report.image_identity.repo_runtime_digest - if report.image_identity - else "not captured" - ), + "endpoint": config.repo_base_url if config else "", + "configured_image": config.repo_image if config else "", + "runtime_digest": runtime.repo_runtime_digest if runtime else "NOT_CAPTURED", }, "registry": { - "endpoint": report.config.registry_base_url if report.config else "", - "configured_image": report.config.registry_image if report.config else "", - "runtime_digest": ( - report.image_identity.registry_runtime_digest - if report.image_identity - else "not captured" - ), + "endpoint": config.registry_base_url if config else "", + "configured_image": config.registry_image if config else "", + "runtime_digest": runtime.registry_runtime_digest if runtime else "NOT_CAPTURED", }, }, - "image_identity_error": report.image_identity_error, "runtime_identity": { - "status": "CAPTURED" if report.image_identity else "NOT_CAPTURED", - "reason": report.image_identity_error or "No Compose project was supplied for capture.", + "status": "CAPTURED" if runtime else "NOT_CAPTURED", + "reason": report.image_identity_error, }, "steps": steps, - "summary": { - "total": len(steps), - "passed": counts["PASS"], - "expected_error": counts["EXPECTED_ERROR"], - "blocked": counts["BLOCKED"], - "skipped": counts["SKIP"], - "fail": counts["FAIL"], - "steps_completed": len(steps) - counts["BLOCKED"] - counts["SKIP"], - "expected_errors_demonstrated": counts["EXPECTED_ERROR"], - "blocked_steps": counts["BLOCKED"], - "skipped_steps": counts["SKIP"], - "unexpected_failures": counts["FAIL"], - }, + "summary": summary, "cleanup": { "repository": report.context.repository_cleanup, "public_sdk_deleted": ( - [f"repository_dpp:{report.context.dpp_id}"] + [f"repository_dpp:{report.identity.dpp_id}"] if report.context.repository_cleanup == "deleted" else [] ), "post_delete_result": report.context.post_delete_result, - "registry": "not_supported_public_api", - "registry_state": report.context.registry_state, - "docker_project_cleanup": "separate operator responsibility", + "registry": "public registration endpoint has no deregistration operation", + "registration_id": report.context.registration_id, "warnings": list(report.cleanup_warnings), }, "strict_verification": {"status": "NOT_RUN", "next_command": "verify"}, - "verdict": ( - "LIVE_DEMONSTRATION_PASSED" - if report.profile == "demo" and not counts["FAIL"] and not counts["BLOCKED"] - else "LIVE_DEMONSTRATION_BLOCKED" - if report.profile == "demo" and counts["BLOCKED"] and not counts["FAIL"] - else "LIVE_DEMONSTRATION_FAILED" - if report.profile == "demo" - else "LIVE_INTEGRATION_DEMONSTRATION_PASSED" - if not counts["FAIL"] and not counts["BLOCKED"] - else "LIVE_INTEGRATION_DEMONSTRATION_FAILED" - ), - "exit_outcome": "SUCCESS" if not counts["FAIL"] and not counts["BLOCKED"] else "FAILURE", + "verdict": "LIVE_DEMONSTRATION_PASSED" + if summary["fail"] == 0 and summary["blocked"] == 0 + else "LIVE_DEMONSTRATION_FAILED", + "exit_outcome": "SUCCESS" + if summary["fail"] == 0 and summary["blocked"] == 0 + else "FAILURE", } def render_integration_text(report: IntegrationReport) -> str: payload = integration_payload(report) lines = [ - "DPP Python SDK - Live Mock-service Integration Demonstration", - "=" * 52, + "DPP Python SDK - Live Mock-service Interoperability Example", f"Python SDK: {payload['sdk']['version']}", - "Mode: Live educational walkthrough", - "Scope: selected public SDK calls against already-running mock services.", + "Scope: typed DPP4Fun preparation and neutral JSON HTTP clients.", "Configured images:", " repository: " f"{payload['services']['repository']['configured_image'] or 'not configured'}", f" registry: {payload['services']['registry']['configured_image'] or 'not configured'}", - "Runtime digests:", - f" repository: {payload['services']['repository']['runtime_digest']}", - f" registry: {payload['services']['registry']['runtime_digest']}", ] - if payload["runtime_identity"]["status"] == "NOT_CAPTURED": - lines.append(f" reason: {payload['runtime_identity']['reason']}") - stage_headings = { - "environment": "Environment and readiness", - "local": "Local DPP preparation", - "create": "Repository creation and reads", - "read": "Repository creation and reads", - "update": "Updates and history", - "history": "Updates and history", - "fine_grained": "Fine-grained access", - "registry": "Registry interaction", - "error": "Expected errors", - "cleanup": "Cleanup", - "blocked": "Blocked steps", - } - previous_heading = "" for step in report.steps: - heading = stage_headings.get(step.stage, step.stage.replace("_", " ").title()) - if heading != previous_heading: - lines += ["", heading, "-" * len(heading)] - previous_heading = heading - lines += [ + lines.extend( + ( + "", + f"[{step.step_id}] {step.title}", + f"Status: {step.status}", + f"Purpose: {step.purpose}", + f"Operation: {step.operation['display']}", + f"Service interaction: {step.service['interaction']}", + f"Observed result: {step.response}", + f"Evidence: {step.persistence_proof}", + f"Why this matters: {step.why_it_matters}", + ) + ) + cleanup = payload["cleanup"] + lines.extend( + ( "", - f"[{step.step_id}] {step.title}", - f"Status: {step.status}", - f"Evidence: {step.evidence_class}", - "Purpose", - f" {step.purpose}", - "Input", - ] - lines += [f" {k}: {v}" for k, v in step.input.items()] - lines += ["SDK operation", f" {step.operation['display']}"] - lines += ["Service interaction", f" {step.service['interaction']}"] - lines += ["Observed result"] - lines += [f" {k}: {v}" for k, v in step.response.items()] - lines += ( - ["Persistence proof"] - + [f" {k}: {v}" for k, v in step.persistence_proof.items()] - + [ - "Explanation", - f" {step.explanation}", - "Why this matters", - f" {step.why_it_matters}", - ] + "Cleanup report:", + f" repository={cleanup['repository']}; post_delete={cleanup['post_delete_result']}", + f" registry registration={cleanup['registration_id'] or 'not created'}; " + "registry deregistration is not a supported operation", + "Strict verification: NOT_RUN (separate command).", ) - lines += [ - "", - "Summary:", - " steps completed={steps_completed}; expected errors demonstrated=" - "{expected_errors_demonstrated}; blocked steps={blocked_steps}; skipped steps=" - "{skipped_steps}; unexpected failures={unexpected_failures}".format(**payload["summary"]), - "Cleanup report:", - " repository={repository}; post_delete={post_delete_result}; public_sdk_deleted=" - "{public_sdk_deleted}".format(**payload["cleanup"]), - " registry=not supported by public SDK; state={registry_state}; docker project cleanup=" - "separate operator responsibility".format(**payload["cleanup"]), - "Strict verification: NOT_RUN (separate command).", - "Next step: run verify with --compose-project and --sdk-wheel for strict evidence.", - ] + ) return "\n".join(lines) diff --git a/examples/mock-services-demo/src/dpp_mock_services_demo/readiness.py b/examples/mock-services-demo/src/dpp_mock_services_demo/readiness.py index 81a9cf5..8849dab 100644 --- a/examples/mock-services-demo/src/dpp_mock_services_demo/readiness.py +++ b/examples/mock-services-demo/src/dpp_mock_services_demo/readiness.py @@ -1,60 +1,64 @@ -"""Bounded functional readiness probes for already-running public services.""" +"""Bounded HTTP readiness probes for already-running public services.""" from __future__ import annotations from collections.abc import Callable from time import monotonic, sleep -from typing import Protocol - -from .reporting import ScenarioResult, ScenarioStatus +import httpx -class HealthClient(Protocol): - def health_check(self) -> bool: ... +from .reporting import ScenarioResult, ScenarioStatus def wait_for_health( *, scenario_id: str, name: str, - client: HealthClient, - timeout_seconds: float, endpoint: str, + timeout_seconds: float, + client: httpx.Client | None = None, clock: Callable[[], float] = monotonic, pause: Callable[[float], object] = sleep, interval_seconds: float = 0.25, ) -> ScenarioResult: - """Poll public health behavior until success or the configured deadline.""" - + """Poll ``GET /health`` until HTTP succeeds or the configured deadline.""" + owned = client is None + http = client or httpx.Client(timeout=2.0) started = clock() deadline = started + timeout_seconds attempts = 0 last_error = "" - while True: - attempts += 1 - try: - if client.health_check(): + url = endpoint.rstrip("/") + "/health" + try: + while True: + attempts += 1 + try: + response = http.get(url) + if response.is_success: + return ScenarioResult( + scenario_id=scenario_id, + name=name, + category="LIVE_MOCK_IMAGE", + status=ScenarioStatus.PASSED, + duration_seconds=max(0.0, clock() - started), + summary=f"HTTP readiness succeeded after {attempts} attempt(s)", + details=f"GET {url} returned HTTP {response.status_code}", + ) + last_error = f"HTTP {response.status_code}" + except httpx.HTTPError as exc: + last_error = f"{type(exc).__name__}: {exc}" + now = clock() + if now >= deadline: return ScenarioResult( scenario_id=scenario_id, name=name, - category="LIVE_050", - status=ScenarioStatus.PASSED, - duration_seconds=max(0.0, clock() - started), - summary=f"Functional readiness succeeded after {attempts} attempt(s)", - details=f"GET {endpoint.rstrip('/')}/health", + category="LIVE_MOCK_IMAGE", + status=ScenarioStatus.FAILED, + duration_seconds=max(0.0, now - started), + summary=f"HTTP readiness timed out after {attempts} attempt(s)", + details=f"{url}: {last_error}", ) - last_error = "health endpoint returned a non-success response" - except Exception as exc: # noqa: BLE001 - retained as readiness failure context - last_error = f"{type(exc).__name__}: {exc}" - now = clock() - if now >= deadline: - return ScenarioResult( - scenario_id=scenario_id, - name=name, - category="LIVE_050", - status=ScenarioStatus.FAILED, - duration_seconds=max(0.0, now - started), - summary=f"Functional readiness timed out after {attempts} attempt(s)", - details=f"{endpoint.rstrip('/')}/health: {last_error}", - ) - pause(min(interval_seconds, deadline - now)) + pause(min(interval_seconds, deadline - now)) + finally: + if owned: + http.close() diff --git a/examples/mock-services-demo/src/dpp_mock_services_demo/registry_scenarios.py b/examples/mock-services-demo/src/dpp_mock_services_demo/registry_scenarios.py index 95866fe..3039165 100644 --- a/examples/mock-services-demo/src/dpp_mock_services_demo/registry_scenarios.py +++ b/examples/mock-services-demo/src/dpp_mock_services_demo/registry_scenarios.py @@ -1,53 +1,35 @@ -"""REG-01 through REG-07 against an already-running Mock registry service.""" +"""Registration endpoint examples for an already-running Mock registry.""" from __future__ import annotations from collections.abc import Callable from time import perf_counter from typing import Protocol -from uuid import uuid4 -from dpp_sdk import Dpp4Fun, Dpp4FunJsonCodec, validate_dpp4fun from dpp_sdk.clients import ( - CreateDppResponse, - DeleteDppResponse, + DppApiClientError, DppHttpClientError, DppRegistryClient, - DppRepoClient, RegisterDppRequest, - RegisterDppResponse, ) from .config import DemoConfig -from .fixtures import DemoIdentity, build_complete_fixture +from .fixtures import DemoIdentity from .readiness import wait_for_health from .reporting import LiveRun, ScenarioResult, ScenarioStatus -class RegistryRepositoryClient(Protocol): - def create_dpp(self, dpp: Dpp4Fun) -> CreateDppResponse: ... - - def delete_dpp_by_id(self, dpp_id: str) -> DeleteDppResponse: ... - - def close(self) -> None: ... - - class RegistryClient(Protocol): - def health_check(self) -> bool: ... - - def post_new_dpp_to_registry(self, request: RegisterDppRequest) -> RegisterDppResponse: ... + def post_new_dpp_to_registry(self, request: RegisterDppRequest) -> object: ... def close(self) -> None: ... _NAMES = { - "REG-01": "Registry functional readiness", - "REG-02": "Register repository-backed DPP", - "REG-03": "Prove repository-backed verification", - "REG-04": "Reject duplicate registration", - "REG-05": "Reject missing repository DPP", - "REG-06": "Reject unavailable repository", - "REG-07": "Reject invalid registration request", + "REG-01": "Registry HTTP readiness", + "REG-02": "Register DPP JSON request", + "REG-03": "Reject duplicate registration", + "REG-04": "Reject invalid registration request", } @@ -55,204 +37,127 @@ def _result(scenario_id: str, operation: Callable[[], str]) -> ScenarioResult: started = perf_counter() try: details = operation() - except Exception as exc: # noqa: BLE001 - live boundary records unexpected category + except Exception as exc: # noqa: BLE001 - report live boundary errors by category return ScenarioResult( - scenario_id=scenario_id, - name=_NAMES[scenario_id], - category="LIVE_050", - status=ScenarioStatus.FAILED, - duration_seconds=perf_counter() - started, - summary="Live registry assertion failed", - details=f"{type(exc).__name__}: {exc}", + scenario_id, + _NAMES[scenario_id], + "LIVE_MOCK_IMAGE", + ScenarioStatus.FAILED, + perf_counter() - started, + "Live registry assertion failed", + f"{type(exc).__name__}: {exc}", ) return ScenarioResult( - scenario_id=scenario_id, - name=_NAMES[scenario_id], - category="LIVE_050", - status=ScenarioStatus.PASSED, - duration_seconds=perf_counter() - started, - summary="Live registry assertion passed", - details=details, + scenario_id, + _NAMES[scenario_id], + "LIVE_MOCK_IMAGE", + ScenarioStatus.PASSED, + perf_counter() - started, + "Live registry assertion passed", + details, ) -def _expected_http(operation: Callable[[], object], status_code: int, context: str) -> None: +def _expect_http(operation: Callable[[], object], status: int, context: str) -> None: try: operation() - except DppHttpClientError as exc: - if exc.status_code != status_code: - raise AssertionError( - f"{context}: expected HTTP {status_code}, got {exc.status_code}" - ) from exc - return - raise AssertionError(f"{context}: expected DppHttpClientError HTTP {status_code}") - - -def _fail_remaining(start: int, reason: str) -> dict[str, ScenarioResult]: - return { - f"REG-{index:02d}": ScenarioResult( - scenario_id=f"REG-{index:02d}", - name=_NAMES[f"REG-{index:02d}"], - category="LIVE_050", - status=ScenarioStatus.FAILED, - duration_seconds=0.0, - summary="Required registry prerequisite failed", - details=reason, - ) - for index in range(start, 8) - } + except (DppHttpClientError, DppApiClientError) as exc: + actual_status = exc.http_status if isinstance(exc, DppApiClientError) else exc.status_code + if actual_status == status: + return + raise AssertionError(f"{context}: expected HTTP {status}, got {actual_status}") from exc + raise AssertionError(f"{context}: expected HTTP {status}") + + +def _request(identity: DemoIdentity, config: DemoConfig) -> RegisterDppRequest: + return RegisterDppRequest( + unique_product_identifier=identity.product_id, + digital_product_passport_id=str(identity.dpp_id), + unique_economic_operator_identifier=identity.registry_sensitive_id, + dpp_api_endpoint=f"{config.repo_base_url.rstrip('/')}/v1/dpps", + ) def run_registry_scenarios( config: DemoConfig, identity: DemoIdentity, *, - repo_client: RegistryRepositoryClient | None = None, registry_client: RegistryClient | None = None, + readiness_client=None, ) -> LiveRun: - """Create, register, exercise natural negatives, and clean the repository DPP.""" - - own_repo = repo_client is None - own_registry = registry_client is None - repo = repo_client or DppRepoClient( - config.repo_base_url, - Dpp4FunJsonCodec(), - validate_dpp4fun, - ) + """Exercise registry registration without asserting repository lookup behavior.""" + owned = registry_client is None registry = registry_client or DppRegistryClient(config.registry_base_url) - fixture = build_complete_fixture(identity) - dpp_id = fixture.dpp_id + request = _request(identity, config) results: dict[str, ScenarioResult] = {} - cleanup_warnings: list[str] = [] - active = False try: results["REG-01"] = wait_for_health( scenario_id="REG-01", name=_NAMES["REG-01"], - client=registry, - timeout_seconds=config.startup_timeout_seconds, endpoint=config.registry_base_url, + timeout_seconds=config.startup_timeout_seconds, + client=readiness_client, ) if results["REG-01"].status is not ScenarioStatus.PASSED: - results.update(_fail_remaining(2, "REG-01 readiness failed")) - return LiveRun(tuple(results[f"REG-{index:02d}"] for index in range(1, 8))) - - def create_repository_dpp() -> None: - nonlocal active - validate_dpp4fun(fixture) - created = repo.create_dpp(fixture) - if created.dppId != dpp_id: - raise AssertionError(f"repository created unexpected dppId {created.dppId}") - active = True - - try: - create_repository_dpp() - except Exception as exc: # noqa: BLE001 - prerequisite becomes explicit scenario failures - failure = f"repository create failed: {type(exc).__name__}: {exc}" - results.update(_fail_remaining(2, failure)) - return LiveRun(tuple(results[f"REG-{index:02d}"] for index in range(1, 8))) + for scenario_id in ("REG-02", "REG-03", "REG-04"): + results[scenario_id] = ScenarioResult( + scenario_id, + _NAMES[scenario_id], + "LIVE_MOCK_IMAGE", + ScenarioStatus.FAILED, + 0, + "Registry readiness prerequisite failed", + "REG-01 failed", + ) + return LiveRun(tuple(results[f"REG-{index:02d}"] for index in range(1, 5))) - request = RegisterDppRequest( - uniqueProductIdentifier=fixture.product_id, - digitalProductPassportId=dpp_id, - uniqueEconomicOperatorIdentifier=identity.registry_sensitive_id, - dppApiEndpoint=config.repo_base_url, - ) + registration_id = "" def register() -> str: + nonlocal registration_id response = registry.post_new_dpp_to_registry(request) - if response.registrationId is None or not response.registrationId.strip(): + registration_id = response.registration_id or "" + if not registration_id.strip(): raise AssertionError("registrationId is missing or blank") - return f"registrationId={response.registrationId}" + return f"registration_id={registration_id}" results["REG-02"] = _result("REG-02", register) + if results["REG-02"].status is ScenarioStatus.PASSED: + results["REG-03"] = _result( + "REG-03", + lambda: ( + _expect_http( + lambda: registry.post_new_dpp_to_registry(request), + 409, + "duplicate registration", + ) + or "duplicate request returned HTTP 409" + ), + ) + else: + results["REG-03"] = ScenarioResult( + "REG-03", + _NAMES["REG-03"], + "LIVE_MOCK_IMAGE", + ScenarioStatus.FAILED, + 0, + "Registration prerequisite failed", + "REG-02 did not register", + ) + + invalid_request = request.model_copy(update={"unique_product_identifier": ""}) results["REG-04"] = _result( "REG-04", lambda: ( - _expected_http( - lambda: registry.post_new_dpp_to_registry(request), - 409, - "duplicate registration", - ) - or "duplicate registration returned HTTP 409" - ), - ) - - missing_request = RegisterDppRequest( - uniqueProductIdentifier=f"missing-product-{uuid4().hex}", - digitalProductPassportId=str(uuid4()), - uniqueEconomicOperatorIdentifier=f"missing-operator-{uuid4().hex}", - dppApiEndpoint=config.repo_base_url, - ) - results["REG-05"] = _result( - "REG-05", - lambda: ( - _expected_http( - lambda: registry.post_new_dpp_to_registry(missing_request), - 404, - "missing repository DPP", - ) - or "missing repository DPP returned HTTP 404" - ), - ) - - def verification_evidence() -> str: - if results["REG-02"].status is not ScenarioStatus.PASSED: - raise AssertionError("successful repository-backed registration was not proven") - if results["REG-05"].status is not ScenarioStatus.PASSED: - raise AssertionError("missing repository DPP rejection was not proven") - return "REG-02 success plus REG-05 HTTP 404 proves repository-backed verification" - - results["REG-03"] = _result("REG-03", verification_evidence) - - unavailable_request = RegisterDppRequest( - uniqueProductIdentifier=f"unreachable-product-{uuid4().hex}", - digitalProductPassportId=str(uuid4()), - uniqueEconomicOperatorIdentifier=f"unreachable-operator-{uuid4().hex}", - dppApiEndpoint="http://127.0.0.1:1", - ) - results["REG-06"] = _result( - "REG-06", - lambda: ( - _expected_http( - lambda: registry.post_new_dpp_to_registry(unavailable_request), - 502, - "unavailable repository", - ) - or "container-local unreachable repository returned HTTP 502" - ), - ) - - invalid_request = RegisterDppRequest( - uniqueProductIdentifier="", - digitalProductPassportId=str(uuid4()), - uniqueEconomicOperatorIdentifier=f"invalid-operator-{uuid4().hex}", - dppApiEndpoint=config.repo_base_url, - ) - results["REG-07"] = _result( - "REG-07", - lambda: ( - _expected_http( + _expect_http( lambda: registry.post_new_dpp_to_registry(invalid_request), 400, - "invalid request", + "blank required request field", ) - or "blank required value returned HTTP 400" + or "invalid request returned HTTP 400" ), ) finally: - if active: - try: - repo.delete_dpp_by_id(dpp_id) - active = False - except Exception as exc: # noqa: BLE001 - cleanup warning is report evidence - cleanup_warnings.append( - f"registry-flow repository cleanup for {dpp_id}: {type(exc).__name__}: {exc}" - ) - if own_registry: + if owned: registry.close() - if own_repo: - repo.close() - - ordered = tuple(results[f"REG-{index:02d}"] for index in range(1, 8)) - return LiveRun(ordered, tuple(cleanup_warnings)) + return LiveRun(tuple(results[f"REG-{index:02d}"] for index in range(1, 5))) diff --git a/examples/mock-services-demo/src/dpp_mock_services_demo/reporting.py b/examples/mock-services-demo/src/dpp_mock_services_demo/reporting.py index 711dcd1..811c2c8 100644 --- a/examples/mock-services-demo/src/dpp_mock_services_demo/reporting.py +++ b/examples/mock-services-demo/src/dpp_mock_services_demo/reporting.py @@ -76,10 +76,10 @@ class FullOperationEvidence: _FULL_PUBLIC_OPERATIONS = { - "REP-01": "DppRepoClient.health_check()", - "REP-02": "DppRepoClient.create_dpp(dpp)", + "REP-01": "HTTP GET /health", + "REP-02": "DppRepoClient.create_dpp(json_value)", "REP-03": "DppRepoClient.read_dpp_by_id(dpp_id)", - "REP-04": "DppRepoClient.read_compressed_dpp_by_id(dpp_id)", + "REP-04": "DppRepoClient.read_dpp_by_id(dpp_id, FULL)", "REP-05": "DppRepoClient.read_dpp_by_product_id(product_id)", "REP-06": "DppRepoClient.read_dpp_version_by_id_and_date(dpp_id, at)", "REP-07": "DppRepoClient.read_dpp_ids_by_product_ids(product_ids, limit, cursor)", @@ -87,17 +87,15 @@ class FullOperationEvidence: "REP-09": "DppRepoClient.read_data_element(dpp_id, selector)", "REP-10": "DppRepoClient.update_data_element(dpp_id, selector, value)", "REP-11": "DppRepoClient.delete_dpp_by_id(dpp_id)", - "REP-12": "DppRepoClient.create_dpp(dpp)", + "REP-12": "DppRepoClient.create_dpp(json_value)", "REP-13": "DppRepoClient.read_dpp_by_id/read_dpp_by_product_id", "REP-14": "DppRepoClient.update_dpp_by_id(dpp_id, patch)", "REP-15": "DppRepoClient.read_data_element/update_data_element", - "REG-01": "DppRegistryClient.health_check()", + "REG-01": "HTTP GET /health", "REG-02": "DppRegistryClient.post_new_dpp_to_registry(request)", "REG-03": "DppRegistryClient.post_new_dpp_to_registry(request)", "REG-04": "DppRegistryClient.post_new_dpp_to_registry(request)", - "REG-05": "DppRegistryClient.post_new_dpp_to_registry(request)", - "REG-06": "DppRegistryClient.post_new_dpp_to_registry(request)", - "REG-07": "DppRegistryClient.post_new_dpp_to_registry(request)", + "REG-08": "DppRegistryClient.post_new_dpp_to_registry(request)", } diff --git a/examples/mock-services-demo/src/dpp_mock_services_demo/repository_scenarios.py b/examples/mock-services-demo/src/dpp_mock_services_demo/repository_scenarios.py index ad71784..58a09d6 100644 --- a/examples/mock-services-demo/src/dpp_mock_services_demo/repository_scenarios.py +++ b/examples/mock-services-demo/src/dpp_mock_services_demo/repository_scenarios.py @@ -1,19 +1,22 @@ -"""REP-01 through REP-15 against an already-running Mock repository service.""" +"""Repository API examples using only the final generic JSON client.""" from __future__ import annotations +import json from collections.abc import Callable from datetime import UTC, datetime -from time import monotonic, perf_counter, sleep -from typing import Any, Protocol -from uuid import uuid4 +from time import perf_counter +from typing import Protocol -from dpp_sdk import Dpp4Fun, Dpp4FunJsonCodec, validate_dpp4fun +import httpx +from dpp_sdk import Dpp4FunJsonCodec from dpp_sdk.clients import ( CreateDppResponse, - DeleteDppResponse, - DppHttpClientError, + DppApiClientError, + DppApiResponse, + DppClientError, DppRepoClient, + DppRepresentation, ReadDppIdsResponse, ) @@ -24,137 +27,109 @@ class RepositoryClient(Protocol): - def health_check(self) -> bool: ... + def create_dpp( + self, dpp: object, representation: DppRepresentation = ... + ) -> CreateDppResponse: ... - def create_dpp(self, dpp: Dpp4Fun) -> CreateDppResponse: ... + def read_dpp_by_id( + self, dpp_id: str, representation: DppRepresentation = ... + ) -> DppApiResponse: ... - def read_dpp_by_id(self, dpp_id: str) -> Dpp4Fun: ... + def read_dpp_by_product_id( + self, product_id: str, representation: DppRepresentation = ... + ) -> DppApiResponse: ... - def read_compressed_dpp_by_id(self, dpp_id: str) -> Any: ... - - def read_dpp_by_product_id(self, product_id: str) -> Dpp4Fun: ... - - def read_dpp_version_by_id_and_date(self, dpp_id: str, at: datetime) -> Dpp4Fun: ... + def read_dpp_version_by_id_and_date( + self, dpp_id: str, date: datetime, representation: DppRepresentation = ... + ) -> DppApiResponse: ... def read_dpp_ids_by_product_ids( self, product_ids: list[str], limit: int | None = None, cursor: str | None = None ) -> ReadDppIdsResponse: ... - def update_dpp_by_id(self, dpp_id: str, partial_dpp: Any) -> Dpp4Fun: ... + def update_dpp_by_id( + self, dpp_id: str, partial_dpp: object, representation: DppRepresentation = ... + ) -> DppApiResponse: ... - def read_data_element(self, dpp_id: str, element_path: str) -> Any: ... + def read_data_element( + self, dpp_id: str, element_id_path: str, representation: DppRepresentation = ... + ) -> DppApiResponse: ... - def update_data_element(self, dpp_id: str, element_path: str, payload: Any) -> Any: ... + def update_data_element( + self, + dpp_id: str, + element_id_path: str, + data_element: object, + representation: DppRepresentation = ..., + ) -> DppApiResponse: ... - def delete_dpp_by_id(self, dpp_id: str) -> DeleteDppResponse: ... + def delete_dpp_by_id(self, dpp_id: str) -> object: ... def close(self) -> None: ... _NAMES = { - "REP-01": "Repository functional readiness", - "REP-02": "Create DPP", + "REP-01": "Repository HTTP readiness", + "REP-02": "Create DPP JSON", "REP-03": "Read DPP by identifier", - "REP-04": "Read compressed DPP", + "REP-04": "Read explicit FULL representation", "REP-05": "Read DPP by product identifier", - "REP-06": "Read historical DPP snapshots", + "REP-06": "Read historical DPP by date", "REP-07": "Bulk product identifier lookup", - "REP-08": "Merge-patch full DPP", - "REP-09": "Read fine-grained elements", - "REP-10": "Update fine-grained element", - "REP-11": "Delete and verify post-delete state", - "REP-12": "Reject duplicate creation", - "REP-13": "Reject unknown DPP and product", - "REP-14": "Reject invalid merge patch atomically", - "REP-15": "Reject invalid fine-grained selectors", + "REP-08": "PATCH DPP JSON", + "REP-09": "Read a data element", + "REP-10": "PATCH a data element", + "REP-11": "Delete DPP", + "REP-12": "Duplicate create response", + "REP-13": "Unknown DPP response", + "REP-14": "Reject invalid caller JSON", + "REP-15": "Reject blank element path", } -def _result( - scenario_id: str, - operation: Callable[[], str], -) -> ScenarioResult: +def _result(scenario_id: str, operation: Callable[[], str]) -> ScenarioResult: started = perf_counter() try: details = operation() - except Exception as exc: # noqa: BLE001 - live boundary records exact unexpected category - return ScenarioResult( - scenario_id=scenario_id, - name=_NAMES[scenario_id], - category="LIVE_050", - status=ScenarioStatus.FAILED, - duration_seconds=perf_counter() - started, - summary="Live repository assertion failed", - details=f"{type(exc).__name__}: {exc}", - ) + status = ScenarioStatus.PASSED + except Exception as exc: # noqa: BLE001 - capture unexpected live operation failures + details = f"{type(exc).__name__}: {exc}" + status = ScenarioStatus.FAILED return ScenarioResult( - scenario_id=scenario_id, - name=_NAMES[scenario_id], - category="LIVE_050", - status=ScenarioStatus.PASSED, - duration_seconds=perf_counter() - started, - summary="Live repository assertion passed", - details=details, + scenario_id, + _NAMES[scenario_id], + "LIVE_MOCK_IMAGE", + status, + perf_counter() - started, + "Repository assertion passed" + if status is ScenarioStatus.PASSED + else "Repository assertion failed", + details, ) -def _expected_http(operation: Callable[[], object], status_code: int, context: str) -> None: - try: - operation() - except DppHttpClientError as exc: - if exc.status_code != status_code: - raise AssertionError( - f"{context}: expected HTTP {status_code}, got {exc.status_code}" - ) from exc - return - raise AssertionError(f"{context}: expected DppHttpClientError HTTP {status_code}") - - def wait_for_repository( config: DemoConfig, *, - client: RepositoryClient | None = None, - clock: Callable[[], float] = monotonic, - pause: Callable[[float], object] = sleep, + client: httpx.Client | None = None, + clock: Callable[[], float] | None = None, + pause: Callable[[float], object] | None = None, interval_seconds: float = 0.25, ) -> ScenarioResult: - """Wait for repository public health without managing Docker.""" - - owned = client is None - resolved = client or DppRepoClient( - config.repo_base_url, - Dpp4FunJsonCodec(), - validate_dpp4fun, - ) - try: - return wait_for_health( - scenario_id="REP-01", - name=_NAMES["REP-01"], - client=resolved, - timeout_seconds=config.startup_timeout_seconds, - endpoint=config.repo_base_url, - clock=clock, - pause=pause, - interval_seconds=interval_seconds, - ) - finally: - if owned: - resolved.close() - - -def _dependency_failures(start: int, reason: str) -> dict[str, ScenarioResult]: - return { - f"REP-{index:02d}": ScenarioResult( - scenario_id=f"REP-{index:02d}", - name=_NAMES[f"REP-{index:02d}"], - category="LIVE_050", - status=ScenarioStatus.FAILED, - duration_seconds=0.0, - summary="Required repository prerequisite failed", - details=reason, - ) - for index in range(start, 16) + """Wait on the repository's public HTTP health route.""" + kwargs: dict[str, object] = { + "scenario_id": "REP-01", + "name": _NAMES["REP-01"], + "endpoint": config.repo_base_url, + "timeout_seconds": config.startup_timeout_seconds, + "client": client, + "interval_seconds": interval_seconds, } + if clock is not None: + kwargs["clock"] = clock + if pause is not None: + kwargs["pause"] = pause + return wait_for_health(**kwargs) # type: ignore[arg-type] def run_repository_scenarios( @@ -162,228 +137,184 @@ def run_repository_scenarios( identity: DemoIdentity, *, client: RepositoryClient | None = None, + readiness_client: httpx.Client | None = None, ) -> LiveRun: - """Run the complete maintained repository lifecycle and natural negatives.""" - + """Exercise the supported repository routes with disposable JSON data.""" owned = client is None - resolved = client or DppRepoClient( - config.repo_base_url, - Dpp4FunJsonCodec(), - validate_dpp4fun, - ) + repo = client or DppRepoClient(config.repo_base_url) fixture = build_complete_fixture(identity) - dpp_id = fixture.dpp_id - product_id = fixture.product_id + codec = Dpp4FunJsonCodec() + wire = json.loads(codec.to_json(fixture)) + dpp_id, product_id = str(identity.dpp_id), identity.product_id results: dict[str, ScenarioResult] = {} - cleanup_warnings: list[str] = [] active = False - created_at: datetime | None = None - updated_fixture: Dpp4Fun | None = None + cleanup_warnings: list[str] = [] try: - results["REP-01"] = wait_for_repository(config, client=resolved) + results["REP-01"] = wait_for_repository(config, client=readiness_client) if results["REP-01"].status is not ScenarioStatus.PASSED: - results.update(_dependency_failures(2, "REP-01 readiness failed")) + for index in range(2, 16): + scenario_id = f"REP-{index:02d}" + results[scenario_id] = ScenarioResult( + scenario_id, + _NAMES[scenario_id], + "LIVE_MOCK_IMAGE", + ScenarioStatus.SKIPPED, + 0.0, + "Blocked by readiness", + "REP-01 did not return HTTP success", + ) return LiveRun(tuple(results[f"REP-{index:02d}"] for index in range(1, 16))) def create() -> str: - nonlocal active, created_at - validate_dpp4fun(fixture) - response = resolved.create_dpp(fixture) - if response.dppId != dpp_id: - raise AssertionError(f"expected created dppId {dpp_id}, got {response.dppId}") + nonlocal active + response = repo.create_dpp(wire, DppRepresentation.FULL) + if response.digital_product_passport_id != dpp_id: + raise AssertionError("create response identifier differs from submitted JSON") active = True - created_at = datetime.now(UTC) - return f"created dppId={dpp_id}" + return f"digital_product_passport_id={dpp_id}" results["REP-02"] = _result("REP-02", create) - if results["REP-02"].status is not ScenarioStatus.PASSED: - results.update(_dependency_failures(3, "REP-02 create failed")) - return LiveRun(tuple(results[f"REP-{index:02d}"] for index in range(1, 16))) - - def read_by_id() -> str: - stored = resolved.read_dpp_by_id(dpp_id) - if stored != fixture: - raise AssertionError("decoded DPP differs from the created fixture") - return "decoded domain value equals created fixture" - - results["REP-03"] = _result("REP-03", read_by_id) - - def read_compressed() -> str: - compressed = resolved.read_compressed_dpp_by_id(dpp_id) - if not isinstance(compressed, dict): - actual_type = type(compressed).__name__ - raise AssertionError(f"expected compressed mapping, got {actual_type}") - return f"compressed keys={sorted(compressed)}" - results["REP-04"] = _result("REP-04", read_compressed) - - def read_by_product() -> str: - stored = resolved.read_dpp_by_product_id(product_id) - if stored.dpp_id != dpp_id or stored.product_id != product_id: - raise AssertionError("product lookup returned different identifiers") - return f"productId={product_id} resolves dppId={dpp_id}" - - results["REP-05"] = _result("REP-05", read_by_product) + def read_full(representation: DppRepresentation) -> str: + response = repo.read_dpp_by_id(dpp_id, representation) + if not isinstance(response.payload, dict): + raise AssertionError("repository response payload is not a JSON object") + if response.payload.get("digitalProductPassportId") != dpp_id: + raise AssertionError("repository response has a different DPP identifier") + if representation is DppRepresentation.FULL and response.payload != wire: + raise AssertionError("repository JSON differs from submitted document") + return f"representation={representation.value}; JSON fields={len(response.payload)}" + + results["REP-03"] = _result("REP-03", lambda: read_full(DppRepresentation.COMPRESSED)) + results["REP-04"] = _result("REP-04", lambda: read_full(DppRepresentation.FULL)) + results["REP-05"] = _result( + "REP-05", + lambda: _check_payload_identity(repo.read_dpp_by_product_id(product_id), dpp_id), + ) + results["REP-06"] = _result( + "REP-06", + lambda: _check_payload_identity( + repo.read_dpp_version_by_id_and_date( + dpp_id, datetime.now(UTC), DppRepresentation.FULL + ), + dpp_id, + ), + ) - results["REP-12"] = _result( - "REP-12", - lambda: ( - _expected_http(lambda: resolved.create_dpp(fixture), 409, "duplicate create") - or "duplicate create returned HTTP 409" + def bulk() -> str: + response = repo.read_dpp_ids_by_product_ids([product_id], limit=10, cursor="0") + if dpp_id not in (response.dpp_identifiers or []): + raise AssertionError("bulk response omitted created DPP identifier") + return f"dpp_identifiers={response.dpp_identifiers!r}" + + results["REP-07"] = _result("REP-07", bulk) + results["REP-08"] = _result( + "REP-08", + lambda: _check_payload_identity( + repo.update_dpp_by_id(dpp_id, {"dppStatus": "active"}, DppRepresentation.FULL), + dpp_id, ), ) + characteristics_index = next( + index + for index, element in enumerate(wire["elements"]) + if element["elementId"] == "characteristics" + ) + characteristics = wire["elements"][characteristics_index] + color_index = next( + index + for index, element in enumerate(characteristics["elements"]) + if element["elementId"] == "color" + ) + color_element = characteristics["elements"][color_index] + color_path = f"$.elements[{characteristics_index}].elements[{color_index}]" - def missing_reads() -> str: - _expected_http( - lambda: resolved.read_dpp_by_id(str(uuid4())), - 404, - "unknown DPP", - ) - _expected_http( - lambda: resolved.read_dpp_by_product_id(f"missing-{uuid4().hex}"), - 404, - "unknown product", - ) - return "unknown DPP and product returned HTTP 404" - - results["REP-13"] = _result("REP-13", missing_reads) - - def invalid_patch() -> str: - before = resolved.read_dpp_by_id(dpp_id) - _expected_http( - lambda: resolved.update_dpp_by_id( + results["REP-09"] = _result( + "REP-09", + lambda: _check_element( + repo.read_data_element(dpp_id, color_path, DppRepresentation.FULL) + ), + ) + results["REP-10"] = _result( + "REP-10", + lambda: _check_element( + repo.update_data_element( dpp_id, - {"passportMetadata": {"uniqueProductIdentifier": str(uuid4())}}, - ), - 400, - "immutable identifier patch", - ) - after = resolved.read_dpp_by_id(dpp_id) - if after != before: - raise AssertionError("invalid patch changed the stored record") - return "HTTP 400 and stored record remained unchanged" - - results["REP-14"] = _result("REP-14", invalid_patch) - - def update_full() -> str: - nonlocal updated_fixture - updated_name = f"CIR4FUN Demo Chair updated {identity.run_id.hex[:8]}" - updated_fixture = fixture.with_updates( - characteristics=fixture.characteristics.with_updates(productName=updated_name) - ) - returned = resolved.update_dpp_by_id( - dpp_id, - {"characteristics": {"productName": updated_name}}, - ) - if returned.productName != updated_name: - raise AssertionError("merge patch response did not contain updated product name") - if fixture.productName == updated_name: - raise AssertionError("original fixture was mutated") - stored = resolved.read_dpp_by_id(dpp_id) - if stored.productName != updated_name: - raise AssertionError("updated DPP was not persisted") - return "merge patch persisted while original fixture remained unchanged" - - results["REP-08"] = _result("REP-08", update_full) - - def history() -> str: - if created_at is None or updated_fixture is None: - raise AssertionError("create/update timestamps are unavailable") - historical = resolved.read_dpp_version_by_id_and_date(dpp_id, created_at) - current = resolved.read_dpp_version_by_id_and_date(dpp_id, datetime.now(UTC)) - if historical.productName != fixture.productName: - raise AssertionError("historical snapshot did not resolve the created version") - if current.productName != updated_fixture.productName: - raise AssertionError("current snapshot did not resolve the merge-patched version") - return ( - f"aware UTC history resolved {historical.productName!r} " - f"then {current.productName!r}" - ) - - results["REP-06"] = _result("REP-06", history) - - def bulk_lookup() -> str: - response = resolved.read_dpp_ids_by_product_ids([product_id], limit=10, cursor="0") - if response.dppIdentifiers is None or dpp_id not in response.dppIdentifiers: - raise AssertionError("bulk lookup omitted created dppId") - return f"bulk lookup returned {len(response.dppIdentifiers)} identifier(s)" - - results["REP-07"] = _result("REP-07", bulk_lookup) - - def read_elements() -> str: - current_name = resolved.read_data_element(dpp_id, "$.characteristics.productName") - material_name = resolved.read_data_element( - dpp_id, "$.billOfMaterials.materials[0].name" - ) - if updated_fixture is None or current_name != updated_fixture.productName: - raise AssertionError("productName fine read differs from updated DPP") - if fixture.billOfMaterials is None: - raise AssertionError("complete fixture has no Bill of Materials") - expected_material = fixture.billOfMaterials.materials[0].name - if material_name != expected_material: - raise AssertionError("material fine read differs from fixture") - return "singular member and indexed BOM selectors returned expected values" - - results["REP-09"] = _result("REP-09", read_elements) - - def update_element() -> str: - changed_name = f"Fine update {identity.run_id.hex[:8]}" - returned = resolved.update_data_element( - dpp_id, - "$.characteristics.productName", - changed_name, - ) - if returned != changed_name: - raise AssertionError("fine update response differs from direct scalar body") - read_back = resolved.read_data_element(dpp_id, "$.characteristics.productName") - if read_back != changed_name: - raise AssertionError("fine update was not persisted") - return "direct scalar body returned and persisted" - - results["REP-10"] = _result("REP-10", update_element) - - def fine_errors() -> str: - cases = ( - ("characteristics.productName", 400, "malformed selector"), - ("$.characteristics.missing", 404, "no-match selector"), - ("$.billOfMaterials.materials[*]", 501, "wildcard selector"), - ) - for path, status, context in cases: - _expected_http( - lambda path=path: resolved.read_data_element(dpp_id, path), - status, - context, + color_path, + {**color_element, "value": "green"}, + DppRepresentation.FULL, ) - _expected_http( - lambda: resolved.update_data_element(dpp_id, "$", {}), - 400, - "root replacement", - ) - return "malformed=400, no-match=404, wildcard=501, root replacement=400" - - results["REP-15"] = _result("REP-15", fine_errors) + ), + ) + results["REP-12"] = _expected_failure( + "REP-12", lambda: repo.create_dpp(wire, DppRepresentation.FULL) + ) + results["REP-13"] = _expected_failure( + "REP-13", lambda: repo.read_dpp_by_id("missing-" + identity.run_id.hex) + ) + results["REP-14"] = _expected_failure( + "REP-14", + lambda: repo.create_dpp(None), # type: ignore[arg-type] + ) + results["REP-15"] = _expected_failure("REP-15", lambda: repo.read_data_element(dpp_id, " ")) def delete() -> str: nonlocal active - response = resolved.delete_dpp_by_id(dpp_id) - if response.statusCode is not None and not response.statusCode.is_success: - raise AssertionError(f"delete returned {response.statusCode}") + repo.delete_dpp_by_id(dpp_id) active = False - _expected_http(lambda: resolved.read_dpp_by_id(dpp_id), 404, "post-delete read") - return "delete succeeded and post-delete read returned HTTP 404" + try: + repo.read_dpp_by_id(dpp_id) + except DppApiClientError: + return "delete completed; post-delete read returned API not found" + raise AssertionError("deleted DPP remained readable") results["REP-11"] = _result("REP-11", delete) finally: if active: try: - resolved.delete_dpp_by_id(dpp_id) - active = False - except Exception as exc: # noqa: BLE001 - cleanup warning is report evidence - cleanup_warnings.append( - f"repository cleanup for {dpp_id}: {type(exc).__name__}: {exc}" - ) + repo.delete_dpp_by_id(dpp_id) + except Exception as exc: # noqa: BLE001 - retain cleanup diagnostic + cleanup_warnings.append(f"repository cleanup failed: {type(exc).__name__}: {exc}") if owned: - resolved.close() + repo.close() + return LiveRun( + tuple(results[f"REP-{index:02d}"] for index in range(1, 16)), tuple(cleanup_warnings) + ) + + +def _check_payload_identity(response: DppApiResponse, expected: str) -> str: + payload = response.payload + if not isinstance(payload, dict) or payload.get("digitalProductPassportId") != expected: + raise AssertionError("response payload has an unexpected digitalProductPassportId") + return f"digital_product_passport_id={expected}" + + +def _check_element(response: DppApiResponse) -> str: + if response.payload is None: + raise AssertionError("element response has no payload") + return "data element response contains JSON payload" - ordered = tuple(results[f"REP-{index:02d}"] for index in range(1, 16)) - return LiveRun(ordered, tuple(cleanup_warnings)) + +def _expected_failure(scenario_id: str, operation: Callable[[], object]) -> ScenarioResult: + started = perf_counter() + try: + operation() + except (DppClientError, ValueError, TypeError) as exc: + return ScenarioResult( + scenario_id, + _NAMES[scenario_id], + "LIVE_MOCK_IMAGE", + ScenarioStatus.EXPECTED_ERROR, + perf_counter() - started, + "Expected client boundary error", + type(exc).__name__, + ) + return ScenarioResult( + scenario_id, + _NAMES[scenario_id], + "LIVE_MOCK_IMAGE", + ScenarioStatus.FAILED, + perf_counter() - started, + "Expected an error", + "operation unexpectedly succeeded", + ) diff --git a/examples/mock-services-demo/src/dpp_mock_services_demo/sdk_scenarios.py b/examples/mock-services-demo/src/dpp_mock_services_demo/sdk_scenarios.py index 18393ae..34f6bfc 100644 --- a/examples/mock-services-demo/src/dpp_mock_services_demo/sdk_scenarios.py +++ b/examples/mock-services-demo/src/dpp_mock_services_demo/sdk_scenarios.py @@ -1,926 +1,428 @@ -"""Annotated, assertion-based SDK-01 through SDK-17 capability demonstrations. - -The ``sdk`` command selects SDK-01, SDK-02, SDK-03, SDK-06, and SDK-07 below. -Each small ``_sdk_NN`` function is deliberately self-contained: it makes public -SDK calls, asserts the contract being demonstrated, and returns display-ready -teaching evidence. The remaining scenarios are used by maintainer verification. -""" +"""Annotated, offline examples using the final Python SDK public API.""" from __future__ import annotations import json from collections.abc import Callable -from datetime import date, timedelta from time import perf_counter from typing import NamedTuple from uuid import UUID import httpx from dpp_sdk import ( - BillOfMaterials, - Characteristics, - Dimensions, Dpp4Fun, Dpp4FunJsonCodec, - DppError, - DppMappingError, - DppValidationError, - Material, - OrganizationRole, - from_json, - from_json_and_validate, - to_json, + DppHeader, + Granularity, + SingleValuedDataElement, validate_dpp4fun, - validate_dpp_core, ) from dpp_sdk.clients import ( DppApiClientError, - DppClientError, - DppHttpClientError, + DppApiMessage, + DppApiResponse, DppMappingClientError, - DppNetworkClientError, DppRegistryClient, DppRepoClient, - DppValidationClientError, + DppRepresentation, + DppStatusCode, + MessageType, RegisterDppRequest, ) -from pydantic import ValidationError - -from .fixtures import ( - DemoIdentity, - build_complete_fixture, - build_minimal_fixture, - new_demo_identity, +from dpp_sdk.dpp4fun import ( + DPP4FUN_PROFILE, + ChairCharacteristics, + CribCharacteristics, + Dpp4FunQuantity, + FabricCharacteristics, + LengthUnit, + LockerCharacteristics, + Nameplate, + ParticleBoardCharacteristics, + SofaCharacteristics, ) -from .reporting import ScenarioResult, ScenarioStatus, ScenarioTeaching - -class _ScenarioContext(NamedTuple): - identity: DemoIdentity - complete: Dpp4Fun - minimal: Dpp4Fun +from .fixtures import DemoIdentity, build_complete_fixture +from .reporting import ScenarioResult, ScenarioStatus, ScenarioTeaching class _Outcome(NamedTuple): summary: str - details: str = "" - input: dict[str, object] | None = None - observed_result: dict[str, object] | None = None + input: dict[str, object] + observed_result: dict[str, object] class _Scenario(NamedTuple): scenario_id: str name: str - category: str - run: Callable[[_ScenarioContext], _Outcome] - expected_error: bool = False - group: str = "OTHER" - purpose: str = "" - operation: dict[str, str] | None = None - expected_behavior: str = "" - explanation: str = "" - why_it_matters: str = "" - - -# Keep expected-error handling uniform so every scenario proves both the error -# type and, where relevant, the public diagnostic text shown to a consumer. -def _expect( - exception_type: type[BaseException], - operation: Callable[[], object], - contains: str | None = None, -) -> BaseException: - try: - operation() - except exception_type as exc: - if contains is not None and contains not in str(exc): - raise AssertionError( - f"Expected {exception_type.__name__} containing {contains!r}, got {exc!s}" - ) from exc - return exc - raise AssertionError(f"Expected {exception_type.__name__}") - - -def _sdk_01(context: _ScenarioContext) -> _Outcome: - fixture = context.complete - assert fixture.documentation is not None - assert fixture.billOfMaterials is not None - assert fixture.manufacturer is not None and fixture.manufacturer.contact is not None - assert isinstance(fixture.tags, tuple) - assert isinstance(fixture.features, tuple) - assert isinstance(fixture.billOfMaterials.materials, tuple) - return _Outcome( - "Complete typed model constructed", - "All optional model groups are populated", - { - "product_name": fixture.productName, - "manufacturer": fixture.manufacturer.name, - "documentation_links": 2, - "bill_of_material_entries": len(fixture.billOfMaterials.materials), - }, - { - "model_type": type(fixture).__name__, - "documentation_present": True, - "bill_of_materials_present": True, - }, - ) - - -def _sdk_02(context: _ScenarioContext) -> _Outcome: - fixture = context.minimal - validate_dpp4fun(fixture) - assert fixture.documentation is None - assert fixture.billOfMaterials is None + run: Callable[[DemoIdentity, Dpp4Fun], _Outcome] + expected_error: bool + group: str + purpose: str + operation: dict[str, str] + expected_behavior: str + explanation: str + why_it_matters: str + + +def _typed_passport(identity: DemoIdentity, passport: Dpp4Fun) -> _Outcome: + result = validate_dpp4fun(passport) + if not result.is_valid: + raise AssertionError(f"fixture has semantic issues: {result.issues!r}") return _Outcome( - "Minimal valid model constructed", - "Documentation and BOM are omitted", - { - "product_name": fixture.productName, - "optional_groups": ["documentation", "billOfMaterials"], - }, - { - "semantic_validation": "accepted", - "documentation_present": False, - "bill_of_materials_present": False, - }, + "typed DPP4Fun passport validates", + {"dppId": str(identity.dpp_id), "productId": identity.product_id}, + {"valid": result.is_valid, "aspects": len(passport.passport.data_elements)}, ) -def _sdk_03(context: _ScenarioContext) -> _Outcome: - fixture = context.complete - assert fixture.dpp_id == str(context.identity.dpp_id) - assert fixture.product_id == context.identity.product_id - assert fixture.uniqueProductIdentifier == context.identity.dpp_id - assert fixture.gtinCode == context.identity.product_id +def _immutable_edit(_identity: DemoIdentity, passport: Dpp4Fun) -> _Outcome: + chair = passport.typed_aspect(ChairCharacteristics) + if chair is None: + raise AssertionError("demo fixture has no ChairCharacteristics") + updated = passport.with_updates(aspects=(chair.with_field("color", "green"),)) + updated_chair = updated.typed_aspect(ChairCharacteristics) + if updated_chair is None or chair.field("color").value != "blue": + raise AssertionError("the original aspect was mutated") return _Outcome( - "DPP and product identifiers extracted from public properties", - input={ - "dpp_id_field": str(context.identity.dpp_id), - "product_id_field": context.identity.product_id, - }, - observed_result={ - "dpp_id": fixture.dpp_id, - "product_id": fixture.product_id, - "public_aliases_match": True, - }, + "updated aspect is immutable", + {"field": "characteristics.color", "value": "green"}, + {"original": chair.field("color").value, "updated": updated_chair.field("color").value}, ) -def _sdk_04(context: _ScenarioContext) -> _Outcome: - complete = context.complete - validate_dpp_core(complete.coreDpp) - future_metadata = complete.passportMetadata.with_updates( - passportUpdateDates=(date.today() + timedelta(days=1),) - ) - future_core = complete.coreDpp.with_updates(passportMetadata=future_metadata) - no_organization = complete.nameplate.with_updates(manufacturer=None, supplier=None) - wrong_role = complete.manufacturer - assert wrong_role is not None - wrong_role = wrong_role.with_updates(role=OrganizationRole.SUPPLIER) - bad_role_nameplate = complete.nameplate.with_updates(manufacturer=wrong_role) - contact = complete.manufacturer.contact - assert contact is not None - bad_email = contact.email - assert bad_email is not None - bad_email = bad_email.with_updates(emailAddress="invalid-email") - bad_contact = contact.with_updates(email=bad_email) - bad_contact_manufacturer = complete.manufacturer.with_updates(contact=bad_contact) - bad_contact_nameplate = complete.nameplate.with_updates(manufacturer=bad_contact_manufacturer) - invalid_docs = complete.coreDpp.documentation - assert invalid_docs is not None - invalid_docs = invalid_docs.with_updates( - digitalInstructionsLink=None, - safetyInstructionsLink=None, - downloadable=True, - ) - invalid_cores = ( - future_core, - complete.coreDpp.with_updates(nameplate=no_organization), - complete.coreDpp.with_updates(nameplate=bad_role_nameplate), - complete.coreDpp.with_updates(nameplate=bad_contact_nameplate), - complete.coreDpp.with_updates(documentation=invalid_docs), - ) - for core in invalid_cores: - _expect(DppValidationError, lambda core=core: validate_dpp_core(core)) - return _Outcome( - "Core validation accepted valid data and rejected five semantic violations", - "future date; missing organization; wrong role; invalid email; invalid documentation", - { - "valid_core": "current complete fixture", - "invalid_cases": [ - "future passport update date", - "missing manufacturer and supplier", - "manufacturer with supplier role", - "invalid contact email", - "downloadable documentation without a link", - ], - }, - { - "valid_core": "accepted", - "rejected": [ - { - "input": "future passport update date", - "exception": "DppValidationError", - "path": "passportUpdateDates[0]", - }, - {"input": "missing manufacturer and supplier", "exception": "DppValidationError"}, - {"input": "manufacturer role is SUPPLIER", "exception": "DppValidationError"}, - {"input": "invalid contact email", "exception": "DppValidationError"}, - {"input": "downloadable document without link", "exception": "DppValidationError"}, - ], - }, - ) - - -def _sdk_05(context: _ScenarioContext) -> _Outcome: - complete = context.complete - validate_dpp4fun(complete) - validate_dpp4fun(context.minimal) - future_metadata = complete.passportMetadata.with_updates( - passportUpdateDates=(date.today() + timedelta(days=1),) - ) - invalid_core = complete.coreDpp.with_updates(passportMetadata=future_metadata) - invalid_classification = complete.classification.with_updates(tags=("Chair", " chair ")) - invalid_characteristics = complete.characteristics.with_updates( - features=("adjustable", "ADJUSTABLE") - ) - invalid_bom = BillOfMaterials( - materials=( - Material(name="Steel", portion=0.5, reference="R1"), - Material(name=" steel ", portion=0.5, reference="r1"), - ) - ) - invalid_cross = complete.characteristics.with_updates(productType="Table") - invalid_aggregates = ( - complete.with_updates(coreDpp=invalid_core), - complete.with_updates(classification=invalid_classification), - complete.with_updates(characteristics=invalid_characteristics), - complete.with_updates(billOfMaterials=invalid_bom), - complete.with_updates(characteristics=invalid_cross), - ) - for aggregate in invalid_aggregates: - _expect(DppValidationError, lambda aggregate=aggregate: validate_dpp4fun(aggregate)) - combined = complete.with_updates( - coreDpp=invalid_core, - classification=invalid_classification, - characteristics=invalid_characteristics, - ) - _expect(DppValidationError, lambda: validate_dpp4fun(combined), "passportUpdateDates[0]") - return _Outcome( - "Aggregate validation and fail-fast order demonstrated", - "core; classification; characteristics; BOM; cross-object rule", - { - "invalid_groups": [ - "future core date", - "duplicate tags", - "duplicate features", - "duplicate BOM material", - "product type mismatch", - ] - }, - { - "valid_complete": "accepted", - "all_invalid_groups": "rejected", - "first_combined_error_path": "passportUpdateDates[0]", - }, - ) - - -def _sdk_06(context: _ScenarioContext) -> _Outcome: - complete = context.complete - raw = to_json(complete) - flat = json.loads(raw) - assert "coreDpp" not in flat - assert {"passportMetadata", "nameplate", "documentation"} <= flat.keys() - assert from_json(raw) == complete - nested_raw = complete.model_dump_json() - assert from_json(nested_raw) == complete - duplicate = dict(flat) - duplicate["nameplate"] = {**duplicate["nameplate"], "gtinCode": "flat-loses"} - duplicate["coreDpp"] = complete.coreDpp.model_dump(mode="json") - assert from_json(json.dumps(duplicate)).product_id == complete.product_id - assert from_json_and_validate(raw) == complete - future = complete.with_updates( - coreDpp=complete.coreDpp.with_updates( - passportMetadata=complete.passportMetadata.with_updates( - passportUpdateDates=(date.today() + timedelta(days=1),) - ) - ) - ) - future_raw = to_json(future) - assert from_json(future_raw) == future - _expect(DppValidationError, lambda: from_json_and_validate(future_raw)) +def _codec_roundtrip(_identity: DemoIdentity, passport: Dpp4Fun) -> _Outcome: codec = Dpp4FunJsonCodec() - assert codec.from_json(codec.to_json(complete)) == complete + full = codec.to_expanded_json(passport) + compressed = codec.to_compressed_json(passport) + restored = codec.from_compressed_json(compressed) + if json.loads(codec.to_expanded_json(restored)) != json.loads(full): + raise AssertionError("FULL -> COMPRESSED -> FULL JSON differs") return _Outcome( - "Flat/nested codec and semantic round trip demonstrated", - "nested core wins duplicates; validated parsing remains explicit", - { - "source": {"product_name": complete.productName, "product_id": complete.product_id}, - "json_fragment": { - "productName": flat["characteristics"]["productName"], - "gtinCode": flat["nameplate"]["gtinCode"], - }, - }, - { - "flat_round_trip_equal": from_json(raw) == complete, - "nested_round_trip_equal": from_json(nested_raw) == complete, - "nested_core_precedence": complete.product_id, - "validated_future_date": "DppValidationError", - }, + "canonical FULL and COMPRESSED representations agree", + {"representations": ["FULL", "COMPRESSED"]}, + {"full_elements": len(json.loads(full)["elements"]), "equal": True}, ) -def _sdk_07(context: _ScenarioContext) -> _Outcome: - original = context.complete - changed_characteristics = original.characteristics.with_updates( - productName="Updated Demo Chair" - ) - changed_metadata = original.passportMetadata.with_updates(qrCodeOrDigitalTag="UPDATED-QR") - changed_core = original.coreDpp.with_updates(passportMetadata=changed_metadata) - updated = original.with_updates( - characteristics=changed_characteristics, - coreDpp=changed_core, - ) - validate_dpp4fun(updated) - assert updated is not original - assert type(updated) is type(original) - assert updated.productName == "Updated Demo Chair" - assert original.productName == "CIR4FUN Demo Chair" - assert original.qrCodeOrDigitalTag == context.identity.registry_sensitive_id +def _profile_catalog(_identity: DemoIdentity, _passport: Dpp4Fun) -> _Outcome: + definitions = DPP4FUN_PROFILE.definitions + if len(definitions) != 488: + raise AssertionError(f"unexpected profile definition count: {len(definitions)}") return _Outcome( - "Valid immutable nested updates preserved subtype", - "Original instance remained unchanged", - { - "before": { - "product_name": original.productName, - "qr_code": original.qrCodeOrDigitalTag, - }, - "requested_update": {"product_name": "Updated Demo Chair", "qr_code": "UPDATED-QR"}, - }, - { - "original_product_name": original.productName, - "updated_product_name": updated.productName, - "original_unchanged": original.qrCodeOrDigitalTag - == context.identity.registry_sensitive_id, - "subtype_preserved": type(updated) is type(original), - "semantic_validation": "accepted", - }, + "source-backed semantic profile is available", + {"profile_version": DPP4FUN_PROFILE.metadata["profileVersion"]}, + {"definitions": len(definitions)}, + ) + + +def _characteristics_variants(_identity: DemoIdentity, passport: Dpp4Fun) -> _Outcome: + variants = ( + ChairCharacteristics, + CribCharacteristics, + FabricCharacteristics, + LockerCharacteristics, + ParticleBoardCharacteristics, + SofaCharacteristics, + ) + selected = [] + for variant in variants: + value = Dpp4Fun.create(passport.header, variant.create(color="blue")) + if not validate_dpp4fun(value).is_valid: + raise AssertionError(f"{variant.__name__} fixture is invalid") + selected.append(value.selected_characteristics().variant.value) + return _Outcome( + "all six typed Characteristics variants validate", + {"variants": [item.__name__ for item in variants]}, + {"selected": selected, "count": len(selected)}, ) -def _sdk_08(context: _ScenarioContext) -> _Outcome: - complete = context.complete - _expect(ValidationError, lambda: complete.characteristics.with_updates(productName=" ")) - _expect(ValidationError, lambda: complete.characteristics.with_updates(weight=float("inf"))) - semantic_bad = complete.with_updates( - characteristics=complete.characteristics.with_updates(productType="Table") +def _unit_quantity(_identity: DemoIdentity, _passport: Dpp4Fun) -> _Outcome: + chair = ChairCharacteristics.create().with_quantity( + "height", Dpp4FunQuantity(value=0.82, unit=LengthUnit.METER) ) - _expect(DppValidationError, lambda: validate_dpp4fun(semantic_bad), "inconsistent") + quantity = chair.quantity("height") + if quantity is None or quantity.unit is not LengthUnit.METER: + raise AssertionError("typed height quantity did not preserve its unit") return _Outcome( - "Invalid immutable updates rejected at the correct boundary", - "blank and infinity are structural; cross-object mismatch is semantic", - {"invalid_updates": ["productName=' '", "weight=infinity", "productType='Table'"]}, - { - "blank_product_name": "ValidationError", - "infinite_weight": "ValidationError", - "cross_object_mismatch": "DppValidationError", - "original_unchanged": complete.productType == "Office Chair", - }, - ) - - -def _sdk_09(context: _ScenarioContext) -> _Outcome: - _expect(ValidationError, lambda: Characteristics(productName=" ")) - _expect( - DppValidationError, - lambda: validate_dpp4fun( - context.complete.with_updates( - characteristics=context.complete.characteristics.with_updates(productType="Table") - ) + "quantity retains its semantic unit", + {"value": 0.82, "unit": "m"}, + {"value": quantity.value, "unit": quantity.unit.symbol}, + ) + + +def _extension_preservation(identity: DemoIdentity, _passport: Dpp4Fun) -> _Outcome: + extension = SingleValuedDataElement( + element_id="vendorCode", + object_type="SingleValuedDataElement", + value="V-1", + value_data_type="xsd:string", + ) + value = Dpp4Fun.create( + DppHeader( + digital_product_passport_id=str(identity.dpp_id), + unique_product_identifier=identity.product_id, + granularity=Granularity.MODEL, + dpp_schema_version="1.0", + dpp_status="active", + economic_operator_id=f"operator-{identity.run_id.hex}", ), + Nameplate.create(manufacturerName="Example"), + extensions=(extension,), ) - _expect(DppMappingError, lambda: from_json("null")) - assert issubclass(DppValidationError, DppError) - assert issubclass(DppMappingError, DppError) - client_categories = ( - DppValidationClientError, - DppMappingClientError, - DppNetworkClientError, - DppHttpClientError, - DppApiClientError, - ) - assert all(issubclass(category, DppClientError) for category in client_categories) + codec = Dpp4FunJsonCodec() + restored = codec.from_json(codec.to_json(value)) + if restored.passport.data_elements[-1] != extension: + raise AssertionError("unknown extension changed during transport") return _Outcome( - "Public structural, SDK, mapping, and client error categories demonstrated", - "Five public client error categories share DppClientError", - { - "representative_operations": [ - "Characteristics(productName=' ')", - "validate_dpp4fun(productType mismatch)", - "from_json('null')", - ] - }, - { - "structural_error": "ValidationError", - "semantic_error": "DppValidationError", - "mapping_error": "DppMappingError", - "client_error_base": "DppClientError", - "client_categories": 5, - }, + "unknown compatible element survives transport", + {"elementId": "vendorCode", "dictionaryReference": None}, + {"preserved": True}, ) -def _sdk_10(_context: _ScenarioContext) -> _Outcome: - rejected = (" ", "\t", "\n", "\u00a0", "\u202f", "\u2003") - for value in rejected: - _expect(ValidationError, lambda value=value: Characteristics(productName=value)) - assert Characteristics(productName="\u200b").productName == "\u200b" - mixed = "\u00a0 Demo Chair \u2003" - assert Characteristics(productName=mixed).productName == mixed - return _Outcome( - "Approved Unicode whitespace table demonstrated", - "ASCII/tab/newline/NBSP/narrow-NBSP/em-space reject; U+200B and mixed text retain", - { - "values": [ - {"label": "space", "code_point": "U+0020"}, - {"label": "tab", "code_point": "U+0009"}, - {"label": "newline", "code_point": "U+000A"}, - {"label": "NBSP", "code_point": "U+00A0"}, - {"label": "narrow NBSP", "code_point": "U+202F"}, - {"label": "em space", "code_point": "U+2003"}, - {"label": "zero-width space", "code_point": "U+200B"}, - ] - }, - { - "whitespace_only": "six values rejected with ValidationError", - "zero_width_space": "accepted as content", - "mixed_visible_text": "accepted unchanged", - }, - ) - +def _generic_json_client(identity: DemoIdentity, passport: Dpp4Fun) -> _Outcome: + wire = json.loads(Dpp4FunJsonCodec().to_json(passport)) + requests: list[dict[str, object]] = [] -def _sdk_11(context: _ScenarioContext) -> _Outcome: - assert Characteristics(productName="Chair", weight=0.0).weight == 0.0 - assert Dimensions(width=1.25e2, height=1.0, depth=1.0, unit="cm").width == 125.0 - assert Material(name="Steel", portion=1.25e-2).portion == 0.0125 - for value in (-1.0, float("nan"), float("inf"), float("-inf")): - _expect( - ValidationError, - lambda value=value: Characteristics(productName="Chair", weight=value), + def handler(request: httpx.Request) -> httpx.Response: + requests.append({"method": request.method, "path": request.url.path}) + if json.loads(request.content) != wire: + raise AssertionError("client changed the caller's JSON body") + return httpx.Response( + 201, + json={ + "statusCode": "SuccessCreated", + "payload": {"digitalProductPassportId": str(identity.dpp_id)}, + }, ) - raw = to_json(context.complete) - assert all(token not in raw for token in ("NaN", "Infinity", "-Infinity")) - overflow = json.loads(raw) - overflow["characteristics"]["weight"] = 1e400 - _expect(DppMappingError, lambda: from_json(json.dumps(overflow))) - return _Outcome( - "Finite non-negative numeric contract demonstrated", - "zero/positive/exponent accept; negative/NaN/infinities/overflow reject", - { - "accepted": [0.0, 125.0, 0.0125], - "rejected": ["-1.0", "NaN", "+Infinity", "-Infinity", "JSON overflow"], - }, - { - "zero": "accepted", - "positive": "accepted", - "exponent": "accepted", - "negative": "ValidationError", - "nan": "ValidationError", - "positive_infinity": "ValidationError", - "negative_infinity": "ValidationError", - "overflow": "DppMappingError", - "serialized_json_contains_non_finite_tokens": False, - }, - ) - -def _sdk_12(context: _ScenarioContext) -> _Outcome: - for raw in ("null", "", "{}"): - _expect(DppMappingError, lambda raw=raw: from_json(raw)) - payload = json.loads(to_json(context.complete)) - payload["classification"]["tags"] = [None] - _expect(DppMappingError, lambda: from_json(json.dumps(payload))) - payload["classification"]["tags"] = [" "] - assert from_json(json.dumps(payload)).tags == (" ",) - _expect(DppValidationError, lambda: from_json_and_validate(json.dumps(payload))) - registry = DppRegistryClient("http://localhost:8081") - try: - client_error = _expect( - DppMappingClientError, - lambda: registry.post_new_dpp_to_registry(None), # type: ignore[arg-type] - ) - assert isinstance(client_error.__cause__, ValueError) - finally: - registry.close() - return _Outcome( - "Root, list-member, and required client null contracts demonstrated", - "root null/empty/object and null members map-fail; blank member validates-fail", - { - "codec_roots": ["null", "empty string", "{}"], - "list_members": ["null", "blank string"], - "client_response": "null", - }, - { - "invalid_roots": "DppMappingError", - "null_list_member": "DppMappingError", - "blank_list_member_mapping": "accepted", - "blank_list_member_validation": "DppValidationError", - "registry_null_response": "DppMappingClientError", - "registry_cause": "ValueError", - }, - ) + with httpx.Client(transport=httpx.MockTransport(handler)) as http: + result = DppRepoClient("http://repo.test", client=http).create_dpp(wire) + if result.digital_product_passport_id != str(identity.dpp_id): + raise AssertionError("create response did not map its canonical identifier") + return _Outcome("repository client sends neutral JSON", wire, {"requests": requests}) -def _sdk_13(context: _ScenarioContext) -> _Outcome: - _expect(DppValidationError, lambda: validate_dpp4fun(None), "cannot be null") - complete = context.complete - future_metadata = complete.passportMetadata.with_updates( - passportUpdateDates=(date.today() + timedelta(days=1),) - ) - invalid = complete.with_updates( - coreDpp=complete.coreDpp.with_updates(passportMetadata=future_metadata), - classification=complete.classification.with_updates(tags=("Chair", "chair")), +def _response_message(_identity: DemoIdentity, _passport: Dpp4Fun) -> _Outcome: + message = DppApiMessage(message_type=MessageType.Warning, text="accepted", code="W-1") + response = DppApiResponse( + status_code=DppStatusCode.Success, + payload={"accepted": True}, + message=[message], ) - _expect(DppValidationError, lambda: validate_dpp4fun(invalid), "passportUpdateDates[0]") + if response.message != [message] or response.payload != {"accepted": True}: + raise AssertionError("canonical response envelope did not retain payload/message") return _Outcome( - "Aggregate null guard and supported fail-fast order demonstrated", - "Core validation precedes later aggregate groups", - { - "null_aggregate": None, - "combined_invalid_groups": ["future passport update date", "duplicate tags"], - }, - { - "null_aggregate": "DppValidationError", - "first_combined_error_path": "passportUpdateDates[0]", - "validation_order": "core before classification", - }, + "API response preserves canonical message and payload fields", + {"message": [{"messageType": "Warning", "text": "accepted", "code": "W-1"}]}, + {"statusCode": response.status_code.value, "message_count": len(response.message)}, ) -def _sdk_14(context: _ScenarioContext) -> _Outcome: - empty_bom = context.minimal.with_updates(billOfMaterials=BillOfMaterials()) - bom_json = json.loads(to_json(empty_bom))["billOfMaterials"] - assert bom_json == {"materials": [], "components": [], "parts": []} - duplicate_bom = BillOfMaterials( - materials=( - Material(name="Steel", portion=0.5, reference=" R1 "), - Material(name="\u202fsteel\u2003", portion=0.5, reference="r1"), +def _api_error(_identity: DemoIdentity, _passport: Dpp4Fun) -> _Outcome: + def handler(_request: httpx.Request) -> httpx.Response: + return httpx.Response( + 400, + json={ + "statusCode": "ClientErrorBadRequest", + "message": [{"messageType": "Error", "text": "invalid"}], + }, ) - ) - invalid = context.complete.with_updates(billOfMaterials=duplicate_bom) - _expect(DppValidationError, lambda: validate_dpp4fun(invalid), "duplicate") - null_member = json.loads(to_json(context.complete)) - null_member["billOfMaterials"]["materials"] = [None] - _expect(DppMappingError, lambda: from_json(json.dumps(null_member))) - return _Outcome( - "Bill of Materials arrays and errors demonstrated", - "empty arrays serialize; normalized duplicate and null member reject", - { - "empty_bom": {"materials": [], "components": [], "parts": []}, - "duplicate_materials": ["Steel/R1", "steel/r1"], - "null_material": None, - }, - { - "empty_bom_json": bom_json, - "normalized_duplicate": "DppValidationError", - "null_member": "DppMappingError", - }, - ) - -def _sdk_15(_context: _ScenarioContext) -> _Outcome: - transport = httpx.MockTransport(lambda _request: httpx.Response(200, json={})) - injected = httpx.Client(transport=transport) - codec = Dpp4FunJsonCodec() - with DppRepoClient( - "http://localhost:8080", - codec, - validate_dpp4fun, - client=injected, - ): - pass - with DppRegistryClient("http://localhost:8081", client=injected): - pass - assert not injected.is_closed - injected.close() - owned_repo = DppRepoClient("http://localhost:8080", codec, validate_dpp4fun) - owned_registry = DppRegistryClient("http://localhost:8081") - owned_repo.close() - owned_repo.close() - owned_registry.close() - owned_registry.close() + with httpx.Client(transport=httpx.MockTransport(handler)) as http: + repo = DppRepoClient("http://repo.test", client=http) + try: + repo.read_dpp_by_id("invalid") + except DppApiClientError as exc: + if exc.http_status != 400 or exc.status_code is not DppStatusCode.ClientErrorBadRequest: + raise AssertionError("API error status fields were not preserved") from exc + return _Outcome( + "structured API failure maps to DppApiClientError", + {"http_status": 400}, + {"exception": type(exc).__name__, "status_code": exc.status_code.value}, + ) + raise AssertionError("expected DppApiClientError") + + +def _read_full_and_preserve_ownership(identity: DemoIdentity, passport: Dpp4Fun) -> _Outcome: + wire = json.loads(Dpp4FunJsonCodec().to_json(passport)) + observed: list[str] = [] + + def handler(request: httpx.Request) -> httpx.Response: + observed.append(str(request.url)) + return httpx.Response(200, json={"statusCode": "Success", "payload": wire}) + + http = httpx.Client(transport=httpx.MockTransport(handler)) + repo = DppRepoClient("http://repo.test", client=http) + response = repo.read_dpp_by_id(str(identity.dpp_id), DppRepresentation.FULL) + repo.close() + if http.is_closed: + raise AssertionError("closing a wrapper closed the caller-owned HTTPX client") + http.close() + if "representation=full" not in observed[0] or response.payload != wire: + raise AssertionError("FULL read did not preserve request or response") return _Outcome( - "Client resource ownership demonstrated", - "owned clients close idempotently; injected HTTPX client remains caller-owned", - { - "injected_client": "httpx.Client(MockTransport)", - "owned_clients": ["DppRepoClient", "DppRegistryClient"], - }, - { - "injected_after_sdk_context": "open", - "owned_repo_first_close": "closed", - "owned_repo_second_close": "no error", - "owned_registry_first_close": "closed", - "owned_registry_second_close": "no error", - "caller_closed_injected": True, - }, + "FULL read preserves JSON and caller-owned transport", + {"representation": "full"}, + {"url": observed[0], "caller_client_open_after_wrapper_close": True}, ) -def _sdk_16(context: _ScenarioContext) -> _Outcome: - malformed = json.loads(to_json(context.complete)) - malformed["classification"] = {} - error = _expect(DppMappingError, lambda: from_json(json.dumps(malformed))) - assert error.__cause__ is not None - return _Outcome( - "Invalid codec input preserves its mapping cause", - type(error.__cause__).__name__, - {"malformed_payload": {"classification": {}}}, - { - "public_exception": type(error).__name__, - "underlying_cause": type(error.__cause__).__name__, - "cause_preserved": True, - }, - ) +def _registry_request(identity: DemoIdentity, _passport: Dpp4Fun) -> _Outcome: + observed: list[dict[str, object]] = [] + def handler(request: httpx.Request) -> httpx.Response: + observed.append(json.loads(request.content)) + return httpx.Response( + 200, + json={"statusCode": "Success", "payload": {"registrationId": "registration-1"}}, + ) -def _sdk_17(_context: _ScenarioContext) -> _Outcome: request = RegisterDppRequest( - productIdentifier="GTIN-0001", - dppIdentifier="DPP-1", - operatorIdentifier="operator-1", - repoUrl="https://repo.example.com", - ) - assert request.model_dump()["dppApiEndpoint"] == "https://repo.example.com" + unique_product_identifier=identity.product_id, + digital_product_passport_id=str(identity.dpp_id), + unique_economic_operator_identifier=identity.registry_sensitive_id, + dpp_api_endpoint="http://repo.test/v1/dpps", + ) + with httpx.Client(transport=httpx.MockTransport(handler)) as http: + result = DppRegistryClient("http://registry.test", client=http).post_new_dpp_to_registry( + request + ) + expected = { + "uniqueProductIdentifier", + "digitalProductPassportId", + "uniqueEconomicOperatorIdentifier", + "dppApiEndpoint", + } + if set(observed[0]) != expected or result.registration_id != "registration-1": + raise AssertionError("registry wire contract differs from canonical API") return _Outcome( - "Registry request aliases emit canonical public JSON", - "canonical dppApiEndpoint", - {"constructor_alias": {"repoUrl": "https://repo.example.com"}}, - { - "canonical_output_key": "dppApiEndpoint", - "canonical_output_value": request.model_dump()["dppApiEndpoint"], - }, + "registry request uses canonical JSON names", + request.model_dump(), + {"keys": sorted(expected)}, ) -SCENARIOS: list[_Scenario] = [ - _Scenario( +def _invalid_registry_request(_identity: DemoIdentity, _passport: Dpp4Fun) -> _Outcome: + with httpx.Client(transport=httpx.MockTransport(lambda _request: httpx.Response(500))) as http: + registry = DppRegistryClient("http://registry.test", client=http) + try: + registry.post_new_dpp_to_registry(None) # type: ignore[arg-type] + except DppMappingClientError as exc: + return _Outcome( + "invalid caller input is rejected before I/O", + {"request": None}, + {"exception": type(exc).__name__, "sent": False}, + ) + raise AssertionError("expected DppMappingClientError") + + +def _scenario( + scenario_id: str, + name: str, + run: Callable[[DemoIdentity, Dpp4Fun], _Outcome], + public_api: str, + display: str, + *, + expected_error: bool = False, +) -> _Scenario: + return _Scenario( + scenario_id, + name, + run, + expected_error, + "PYTHON_SDK", + name, + {"public_api": public_api, "display": display}, + f"{name} follows the current public SDK contract.", + "The example uses typed models at the application boundary and JSON at HTTP.", + "An executable example makes SDK behavior easy to verify.", + ) + + +SCENARIOS = [ + _scenario( "SDK-01", - "Complete model construction", - "SDK_LOCAL", - _sdk_01, - group="MODELS_AND_IDENTIFIERS", - purpose="Construct a complete typed DPP with supported optional groups.", - operation={"public_api": "Dpp4Fun", "display": "dpp = Dpp4Fun(...)"}, - expected_behavior=( - "The nested model is structurally valid and retains typed optional groups." - ), - explanation="Pydantic constructs the nested model and its immutable collections.", - why_it_matters="Consumers work with typed fields instead of manually assembling raw JSON.", + "Typed DPP4Fun validation", + _typed_passport, + "Dpp4Fun / validate_dpp4fun", + "validate_dpp4fun(value)", ), - _Scenario( + _scenario( "SDK-02", - "Minimal model construction", - "SDK_LOCAL", - _sdk_02, - group="MODELS_AND_IDENTIFIERS", - purpose="Show that optional documentation and BOM groups may be omitted.", - operation={"public_api": "validate_dpp4fun", "display": "validate_dpp4fun(minimal_dpp)"}, - expected_behavior="A minimal supported DPP passes semantic validation.", - explanation="Optional model groups are not required for a valid minimal passport.", - why_it_matters="Consumers can start with the fields their use case actually has.", + "Immutable aspect update", + _immutable_edit, + "Dpp4Fun.with_updates", + "value.with_updates(aspects=...)", ), - _Scenario( + _scenario( "SDK-03", - "Identifier extraction", - "SDK_LOCAL", - _sdk_03, - group="MODELS_AND_IDENTIFIERS", - purpose="Read DPP and product identifiers through public model properties.", - operation={ - "public_api": "Dpp4Fun.dpp_id / product_id", - "display": "dpp.dpp_id; dpp.product_id", - }, - expected_behavior="Public aliases return the canonical identifier values.", - explanation=( - "The SDK exposes stable properties rather than requiring callers to traverse nested " - "fields." - ), - why_it_matters=( - "Application code can retrieve identifiers without coupling to internal model layout." - ), + "FULL and COMPRESSED round trip", + _codec_roundtrip, + "Dpp4FunJsonCodec", + "codec.from_compressed_json(...)", ), - _Scenario( + _scenario( "SDK-04", - "Core semantic validation", - "SDK_LOCAL", - _sdk_04, - True, - "VALIDATION", - "Demonstrate semantic validation after structurally valid construction.", - {"public_api": "validate_dpp_core", "display": "validate_dpp_core(dpp.coreDpp)"}, - "Valid core data passes; each semantic violation raises DppValidationError.", - "Semantic rules cover dates, organization roles, contact data, and documentation " - "consistency.", - "Consumers receive a public validation category before relying on business data.", + "Semantic profile catalog", + _profile_catalog, + "DPP4FUN_PROFILE", + "DPP4FUN_PROFILE.definitions", ), - _Scenario( + _scenario( "SDK-05", - "Aggregate semantic validation", - "SDK_LOCAL", - _sdk_05, - True, - "VALIDATION", - "Demonstrate cross-object validation and deterministic fail-fast order.", - {"public_api": "validate_dpp4fun", "display": "validate_dpp4fun(dpp)"}, - "Aggregate duplicates and mismatches reject with the first supported path.", - "Aggregate validation checks relationships that a single nested model cannot see.", - "A stable first error lets consumers surface consistent remediation.", + "Six Characteristics variants", + _characteristics_variants, + "Characteristics variants", + "variant.create(color=...)", ), - _Scenario( + _scenario( "SDK-06", - "Codec and semantic round trip", - "SDK_LOCAL", - _sdk_06, - False, - "CODECS_AND_MAPPING", - "Serialize a DPP, decode it, and opt into semantic validation.", - { - "public_api": "to_json / from_json / from_json_and_validate", - "display": "decoded = from_json_and_validate(to_json(dpp))", - }, - "Flat and nested payloads round-trip; semantic-invalid JSON rejects only when " - "validated parsing is requested.", - "Mapping and semantic validation are deliberately separate SDK stages.", - "Consumers can choose fast mapping or validated input according to their trust boundary.", + "Typed quantity and unit", + _unit_quantity, + "Dpp4FunQuantity / LengthUnit", + "Dpp4FunQuantity(value, unit)", ), - _Scenario( + _scenario( "SDK-07", - "Valid immutable updates", - "SDK_LOCAL", - _sdk_07, - False, - "IMMUTABLE_UPDATES", - "Change nested values by creating a validated replacement DPP.", - {"public_api": "Dpp4Fun.with_updates", "display": "updated = dpp.with_updates(...)"}, - "The replacement is validated, keeps its subtype, and leaves the original unchanged.", - "The SDK creates a new immutable instance instead of mutating the existing passport.", - "Immutable updates avoid partially changed shared objects.", + "Unknown extension preservation", + _extension_preservation, + "Dpp4FunJsonCodec", + "codec.from_json(codec.to_json(value))", ), - _Scenario( + _scenario( "SDK-08", - "Rejected immutable updates", - "SDK_LOCAL", - _sdk_08, - True, - "IMMUTABLE_UPDATES", - "Show structural and semantic checks applied to requested updates.", - { - "public_api": "with_updates / validate_dpp4fun", - "display": "dpp.with_updates(...); validate_dpp4fun(updated)", - }, - "Blank/infinite values reject structurally; cross-object mismatch rejects semantically.", - "Update construction and aggregate validation protect different contract boundaries.", - "Consumers can distinguish bad field data from invalid relationships.", + "Generic JSON repository client", + _generic_json_client, + "DppRepoClient.create_dpp", + "repo.create_dpp(json_value)", ), - _Scenario( + _scenario( "SDK-09", - "Public error hierarchy", - "SDK_LOCAL", - _sdk_09, - False, - "ERRORS_AND_CLIENT_OWNERSHIP", - "Identify the public error families consumers can catch.", - { - "public_api": "DppError and DppClientError", - "display": "except DppError: ...; except DppClientError: ...", - }, - "Structural, semantic, mapping, and client categories stay distinct.", - "The SDK preserves error categories instead of flattening all failures.", - "Consumers may catch broadly or react to a precise public failure type.", + "Response envelope and messages", + _response_message, + "DppApiResponse / DppApiMessage", + "response.message", ), - _Scenario( - "SDK-10", - "Whitespace contract", - "SDK_LOCAL", - _sdk_10, - True, - "BOUNDARY_CONTRACTS", - "Show which Unicode whitespace-only values are rejected as blank.", - {"public_api": "Characteristics", "display": "Characteristics(productName=value)"}, - "Whitespace-only values reject; zero-width and mixed visible text remain content.", - "Blank checks use the documented Unicode whitespace behavior.", - "Consumers can validate internationalized input predictably.", + _scenario( + "SDK-10", "API failure mapping", _api_error, "DppApiClientError", "repo.read_dpp_by_id(id)" ), - _Scenario( + _scenario( "SDK-11", - "Finite numeric contract", - "SDK_LOCAL", - _sdk_11, - True, - "BOUNDARY_CONTRACTS", - "Show finite, non-negative numeric requirements and JSON safety.", - { - "public_api": "Characteristics / Dimensions / from_json", - "display": "Characteristics(productName='Chair', weight=value)", - }, - "Zero and finite positive values pass; negative/non-finite/overflow values reject.", - "The SDK refuses values that cannot be represented safely in JSON.", - "Consumers avoid emitting invalid numeric payloads.", + "FULL read and client ownership", + _read_full_and_preserve_ownership, + "DppRepresentation.FULL", + "repo.read_dpp_by_id(id, FULL)", ), - _Scenario( + _scenario( "SDK-12", - "Null and root contract", - "SDK_LOCAL", - _sdk_12, - True, - "CODECS_AND_MAPPING", - "Distinguish invalid codec roots, null members, blank members, and a null client response.", - { - "public_api": ( - "from_json / from_json_and_validate / DppRegistryClient.post_new_dpp_to_registry" - ), - "display": "from_json(payload); registry.post_new_dpp_to_registry(None)", - }, - "Invalid roots/null members map-fail; blank member fails semantic validation; client " - "null maps with a cause.", - "The SDK reports whether failure occurred during mapping, semantic validation, or " - "client translation.", - "Consumers get actionable error handling at each input boundary.", - ), - _Scenario( - "SDK-13", - "Aggregate guard and fail-fast order", - "SDK_LOCAL", - _sdk_13, - True, - "VALIDATION", - "Show aggregate null protection and first-error ordering.", - {"public_api": "validate_dpp4fun", "display": "validate_dpp4fun(value)"}, - "None rejects and core validation reports before later aggregate defects.", - "The aggregate validator has an explicit stable validation sequence.", - "Consumers can reliably highlight the first corrective action.", - ), - _Scenario( - "SDK-14", - "Bill of Materials contract", - "SDK_LOCAL", - _sdk_14, - True, - "BOUNDARY_CONTRACTS", - "Show clean empty BOM serialization and protected collection boundaries.", - { - "public_api": "BillOfMaterials / to_json / validate_dpp4fun", - "display": "to_json(dpp.with_updates(billOfMaterials=BillOfMaterials()))", - }, - "Empty categories serialize as arrays; normalized duplicates and null members reject.", - "The SDK normalizes collection identity and blocks invalid wire members.", - "Consumers receive clean payloads and predictable BOM validation.", - ), - _Scenario( - "SDK-15", - "Client resource ownership", - "CONTROLLED", - _sdk_15, - False, - "ERRORS_AND_CLIENT_OWNERSHIP", - "Show the difference between SDK-owned and caller-injected HTTPX clients.", - { - "public_api": "DppRepoClient.close / DppRegistryClient.close", - "display": "client.close()", - }, - "Owned clients close idempotently; injected clients remain caller-owned.", - "Client construction records ownership so context-manager cleanup is safe.", - "Consumers can share injected transports without unexpected closure.", - ), - _Scenario( - "SDK-16", - "Codec mapping cause", - "SDK_LOCAL", - _sdk_16, - True, - "CODECS_AND_MAPPING", - "Show that malformed mapped data retains its underlying cause.", - {"public_api": "from_json", "display": "from_json(malformed_payload)"}, - "Malformed classification raises DppMappingError with a Pydantic ValidationError cause.", - "Mapping translation preserves diagnostic cause information.", - "Consumers can catch the public error while still logging the underlying reason.", - ), - _Scenario( - "SDK-17", - "Registry request construction", - "CONTROLLED", - _sdk_17, - False, - "ERRORS_AND_CLIENT_OWNERSHIP", - "Show an accepted request alias and canonical emitted JSON field.", - { - "public_api": "RegisterDppRequest", - "display": "RegisterDppRequest(repoUrl='https://repo.example.com')", - }, - "The repoUrl input alias emits canonical dppApiEndpoint output.", - "The request model normalizes supported input aliases to the public wire contract.", - "Consumers can migrate input names without sending an incorrect API field.", + "Invalid registry request mapping", + _invalid_registry_request, + "DppRegistryClient.post_new_dpp_to_registry", + "registry.post_new_dpp_to_registry(None)", + expected_error=True, ), ] @@ -928,78 +430,70 @@ def _sdk_17(_context: _ScenarioContext) -> _Outcome: def run_sdk_scenarios( run_id: UUID | None = None, *, scenario_ids: tuple[str, ...] | None = None ) -> tuple[ScenarioResult, ...]: - """Execute all maintained scenarios or an explicitly selected public subset.""" - - identity = DemoIdentity.from_run_id(run_id) if run_id is not None else new_demo_identity() - context = _ScenarioContext( - identity, - build_complete_fixture(identity), - build_minimal_fixture(identity), - ) - results: list[ScenarioResult] = [] + """Run maintained examples or an explicitly selected canonical subset.""" + identity = DemoIdentity.from_run_id(run_id or UUID("12345678-1234-5678-9234-567812345678")) + passport = build_complete_fixture(identity) selected = ( SCENARIOS if scenario_ids is None else [scenario for scenario in SCENARIOS if scenario.scenario_id in scenario_ids] ) - selected_ids = tuple(scenario.scenario_id for scenario in selected) - if scenario_ids is not None and selected_ids != scenario_ids: + if scenario_ids is not None and tuple(item.scenario_id for item in selected) != scenario_ids: raise ValueError("scenario_ids must name supported scenarios in canonical order") + + results: list[ScenarioResult] = [] for scenario in selected: + required = ( + scenario.group, + scenario.purpose, + scenario.operation.get("public_api"), + scenario.operation.get("display"), + scenario.expected_behavior, + scenario.explanation, + scenario.why_it_matters, + ) + if not all(required): + raise AssertionError(f"{scenario.scenario_id} has incomplete teaching metadata") started = perf_counter() try: - outcome = scenario.run(context) + outcome = scenario.run(identity, passport) except Exception as exc: # noqa: BLE001 - scenario boundary reports unexpected failures results.append( ScenarioResult( scenario_id=scenario.scenario_id, name=scenario.name, - category=scenario.category, + category="SDK_LOCAL", status=ScenarioStatus.FAILED, duration_seconds=perf_counter() - started, summary="Scenario raised an unexpected exception", details=f"{type(exc).__name__}: {exc}", ) ) - else: - if outcome.input is None or outcome.observed_result is None: - raise AssertionError( - f"{scenario.scenario_id} did not capture its structured teaching evidence" - ) - if not all( - ( - scenario.purpose, - scenario.operation, - scenario.expected_behavior, - scenario.explanation, - scenario.why_it_matters, - ) - ): - raise AssertionError(f"{scenario.scenario_id} has incomplete teaching metadata") - results.append( - ScenarioResult( - scenario_id=scenario.scenario_id, - name=scenario.name, - category=scenario.category, - status=( - ScenarioStatus.EXPECTED_ERROR - if scenario.expected_error - else ScenarioStatus.PASSED - ), - duration_seconds=perf_counter() - started, - summary=outcome.summary, - details=outcome.details, - teaching=ScenarioTeaching( - group=scenario.group, - evidence_class=scenario.category, - purpose=scenario.purpose, - input=outcome.input, - operation=scenario.operation, - expected_behavior=scenario.expected_behavior, - observed_result=outcome.observed_result, - explanation=scenario.explanation, - why_it_matters=scenario.why_it_matters, - ), - ) + continue + results.append( + ScenarioResult( + scenario_id=scenario.scenario_id, + name=scenario.name, + category="SDK_LOCAL", + status=( + ScenarioStatus.EXPECTED_ERROR + if scenario.expected_error + else ScenarioStatus.PASSED + ), + duration_seconds=perf_counter() - started, + summary=outcome.summary, + details=json.dumps(outcome.observed_result, sort_keys=True), + teaching=ScenarioTeaching( + group=scenario.group, + evidence_class="SDK_LOCAL", + purpose=scenario.purpose, + input=outcome.input, + operation=scenario.operation, + expected_behavior=scenario.expected_behavior, + observed_result=outcome.observed_result, + explanation=scenario.explanation, + why_it_matters=scenario.why_it_matters, + ), ) + ) return tuple(results) diff --git a/examples/mock-services-demo/tests/test_cli.py b/examples/mock-services-demo/tests/test_cli.py index 66a28cd..3a719c3 100644 --- a/examples/mock-services-demo/tests/test_cli.py +++ b/examples/mock-services-demo/tests/test_cli.py @@ -313,7 +313,7 @@ def test_integration_blocked_readiness_writes_json_failure_and_returns_nonzero( file_payload = json.loads(report_path.read_text(encoding="utf-8")) assert console_payload == file_payload assert console_payload["exit_outcome"] == "FAILURE" - assert console_payload["summary"]["blocked_steps"] == 14 + assert console_payload["summary"]["blocked_steps"] == 8 def test_integration_invalid_configuration_and_missing_mode_return_parser_errors( @@ -469,12 +469,12 @@ def test_installed_sdk_provenance_is_bound_to_supplied_wheel( sdk_file = site_packages / "dpp_sdk" / "__init__.py" sdk_file.parent.mkdir(parents=True) sdk_file.write_text("", encoding="utf-8") - wheel = tmp_path / "dpp_sdk-0.4.0-py3-none-any.whl" + wheel = tmp_path / f"dpp_sdk-{cli.dpp_sdk.__version__}-py3-none-any.whl" wheel.write_bytes(b"exact wheel") digest = hashlib.sha256(wheel.read_bytes()).hexdigest() class FakeDistribution: - version = "0.4.0" + version = cli.dpp_sdk.__version__ def locate_file(self, _path: str) -> Path: return site_packages diff --git a/examples/mock-services-demo/tests/test_compose_config.py b/examples/mock-services-demo/tests/test_compose_config.py index bd88504..bb2a78e 100644 --- a/examples/mock-services-demo/tests/test_compose_config.py +++ b/examples/mock-services-demo/tests/test_compose_config.py @@ -40,8 +40,8 @@ def test_compose_matches_mock_postgres_topology_with_public_api_images() -> None assert "dpp-registry-db-data:/var/lib/postgresql/data" in text assert "dpp-repo-db-data:" in text assert "dpp-registry-db-data:" in text - assert "${DPP_REPO_IMAGE:?set DPP_REPO_IMAGE}" in text - assert "${DPP_REGISTRY_IMAGE:?set DPP_REGISTRY_IMAGE}" in text + assert "${DPP_REPO_IMAGE:-ghcr.io/cir4fun-eu/dpp-repo-api:" in text + assert "${DPP_REGISTRY_IMAGE:-ghcr.io/cir4fun-eu/dpp-registry-api:" in text assert '"${MOCK_REPO_PORT:-8080}:${MOCK_REPO_PORT:-8080}"' in text assert '"${MOCK_REGISTRY_PORT:-8081}:${MOCK_REGISTRY_PORT:-8081}"' in text assert "DPP_REPO_BACKEND: postgres" in text diff --git a/examples/mock-services-demo/tests/test_fixtures.py b/examples/mock-services-demo/tests/test_fixtures.py index 8c7b32c..24c012e 100644 --- a/examples/mock-services-demo/tests/test_fixtures.py +++ b/examples/mock-services-demo/tests/test_fixtures.py @@ -2,53 +2,92 @@ from uuid import UUID -from dpp_sdk import Dpp4Fun, validate_dpp4fun +import pytest +from dpp_sdk import Dpp4Fun, Dpp4FunJsonCodec, validate_dpp4fun +from dpp_sdk.core import SingleValuedDataElement +from dpp_sdk.dpp4fun import ( + DPP4FUN_PROFILE, + BillOfMaterials, + ChairCharacteristics, + DesignRecord, + Durability, + Instruction, + Material, + Nameplate, + StandardAssessment, + Traceability, + Usage, +) from dpp_mock_services_demo.fixtures import ( DemoIdentity, build_complete_fixture, build_minimal_fixture, - new_demo_identity, ) -def test_identity_is_unique_per_run_and_repeatable_for_a_run_id() -> None: - first = new_demo_identity() - second = new_demo_identity() - fixed_run = UUID("12345678-1234-5678-9234-567812345678") - - assert first.dpp_id != second.dpp_id - assert first.product_id != second.product_id - assert first.registry_sensitive_id != second.registry_sensitive_id - assert DemoIdentity.from_run_id(fixed_run) == DemoIdentity.from_run_id(fixed_run) - - -def test_minimal_fixture_is_typed_valid_and_uses_run_identity() -> None: - identity = DemoIdentity.from_run_id(UUID("12345678-1234-5678-9234-567812345678")) - fixture = build_minimal_fixture(identity) - - assert isinstance(fixture, Dpp4Fun) - assert fixture.dpp_id == str(identity.dpp_id) - assert fixture.product_id == identity.product_id - assert fixture.documentation is None - assert fixture.billOfMaterials is None - validate_dpp4fun(fixture) - - -def test_complete_fixture_populates_every_optional_group_with_tuples() -> None: - identity = DemoIdentity.from_run_id(UUID("12345678-1234-5678-9234-567812345678")) - fixture = build_complete_fixture(identity) - - assert fixture.documentation is not None - assert fixture.manufacturer is not None - assert fixture.manufacturer.contact is not None - assert fixture.manufacturer.contact.address is not None - assert fixture.manufacturer.contact.email is not None - assert fixture.manufacturer.contact.telephone is not None - assert fixture.billOfMaterials is not None - assert isinstance(fixture.tags, tuple) - assert isinstance(fixture.features, tuple) - assert isinstance(fixture.billOfMaterials.materials, tuple) - assert fixture.billOfMaterials.components - assert fixture.billOfMaterials.parts - validate_dpp4fun(fixture) +@pytest.mark.parametrize("builder", [build_minimal_fixture, build_complete_fixture]) +def test_demo_fixtures_are_valid_current_dpp4fun_aggregates(builder) -> None: + value = builder(DemoIdentity.from_run_id(UUID(int=1))) + + assert isinstance(value, Dpp4Fun) + assert validate_dpp4fun(value).is_valid + assert isinstance(value.typed_aspect(Nameplate), Nameplate) + characteristics = value.selected_characteristics() + assert isinstance(characteristics, ChairCharacteristics) + surface = characteristics.field("surface") + assert isinstance(surface, SingleValuedDataElement) + assert surface.value in {"Smooth", "Woven matte"} + codec = Dpp4FunJsonCodec() + restored = codec.from_json_and_validate(codec.to_json(value)) + assert codec.to_json(restored) == codec.to_json(value) + + +def test_complete_demo_fixture_has_repeated_materials_and_unique_identity() -> None: + identity = DemoIdentity.from_run_id(UUID(int=1)) + value = build_complete_fixture(identity) + bill = value.typed_aspect(BillOfMaterials) + + assert value.header.digital_product_passport_id == str(identity.dpp_id) + assert value.header.unique_product_identifier == identity.product_id + assert bill is not None + repeated = next(item for item in bill.element.elements if item.element_id == "materialSet") + assert len(repeated.value) >= 2 + assert all(Material.from_element(item).field("name").value for item in repeated.value) + + +def test_complete_fixture_covers_general_aspects_multilingual_units_and_records() -> None: + value = build_complete_fixture(DemoIdentity.from_run_id(UUID(int=1))) + expected = set(DPP4FUN_PROFILE.root_element_ids) | {"characteristics"} + actual = {element.element_id for element in value.passport.data_elements} + assert actual == expected + + nameplate = value.typed_aspect(Nameplate) + assert nameplate is not None + designation = nameplate.field("manufacturerProductDesignation") + assert designation is not None + assert {item.language for item in designation.value} == {"en-US", "de-DE"} + + chair = value.selected_characteristics() + assert chair.quantity("height") is not None + assert chair.quantity("height").unit.symbol == "m" + + usage = value.typed_aspect(Usage) + instruction_set = usage.field("assemblyInstructionSet") + assert instruction_set is not None and instruction_set.value + instruction = Instruction.from_element(instruction_set.value[0]) + assert instruction.field("name").value + assert instruction.field("description").value + assert instruction.field("reference").value + + standard_set = value.typed_aspect(Durability).field("standardSet") + assessment = StandardAssessment.from_element(standard_set.value[0]) + assert assessment.field("name").value + assert assessment.field("description").value + + traceability = value.typed_aspect(Traceability) + records = traceability.field("designRecordSet") + record = DesignRecord.from_element(records.value[0]) + assert all( + record.field(name).value for name in ("name", "description", "operator", "time", "status") + ) diff --git a/examples/mock-services-demo/tests/test_installed_sdk_command.py b/examples/mock-services-demo/tests/test_installed_sdk_command.py index e51b9dd..2d2702b 100644 --- a/examples/mock-services-demo/tests/test_installed_sdk_command.py +++ b/examples/mock-services-demo/tests/test_installed_sdk_command.py @@ -161,6 +161,6 @@ def test_installed_sdk_command_needs_no_service_profile_or_checkout(tmp_path: Pa assert maintainer.returncode == 0, maintainer.stderr maintainer_report = json.loads(maintainer.stdout) assert tuple(item["scenario_id"] for item in maintainer_report["results"]) == tuple( - f"SDK-{index:02d}" for index in range(1, 18) + f"SDK-{index:02d}" for index in range(1, 13) ) assert Path(maintainer_report["sdk_location"]).is_relative_to(isolated_environment) diff --git a/examples/mock-services-demo/tests/test_integration_scenarios.py b/examples/mock-services-demo/tests/test_integration_scenarios.py index 5411ce1..1c9b9b1 100644 --- a/examples/mock-services-demo/tests/test_integration_scenarios.py +++ b/examples/mock-services-demo/tests/test_integration_scenarios.py @@ -1,513 +1,183 @@ from __future__ import annotations import json -from datetime import datetime from pathlib import Path -from types import SimpleNamespace from uuid import UUID -from dpp_sdk.clients import DppHttpClientError +from dpp_sdk.clients import ( + CreateDppResponse, + DppApiClientError, + DppApiResponse, + DppStatusCode, + RegisterDppResponse, +) from dpp_mock_services_demo.config import DemoConfig from dpp_mock_services_demo.fixtures import DemoIdentity -from dpp_mock_services_demo.image_identity import RuntimeImageIdentity from dpp_mock_services_demo.integration_scenarios import ( IntegrationRunContext, integration_payload, render_integration_text, run_integration_scenarios, ) - - -class _StatefulRepository: - def __init__(self, *, ready: object = True) -> None: - self.ready = ready - self.original = None - self.current = None - self.history_reads = 0 - self.create_calls = 0 - - def health_check(self) -> object: - return self.ready - - def create_dpp(self, dpp: object) -> SimpleNamespace: - self.create_calls += 1 - self.original = self.current = dpp - return SimpleNamespace(dppId=dpp.dpp_id) - - def _read(self, dpp_id: str) -> object: - if self.current is None or dpp_id != self.current.dpp_id: - raise DppHttpClientError("missing", 404, "{}") - return self.current - - def read_dpp_by_id(self, dpp_id: str) -> object: - return self._read(dpp_id) - - def read_dpp_by_product_id(self, product_id: str) -> object: - if self.current is None or product_id != self.current.product_id: - raise DppHttpClientError("missing", 404, "{}") - return self.current - - def read_dpp_ids_by_product_ids( - self, product_ids: list[str], *, limit: int, cursor: str - ) -> SimpleNamespace: - assert limit == 10 and cursor == "0" - identifiers = [self.current.dpp_id] if self.current.product_id in product_ids else [] - return SimpleNamespace(dppIdentifiers=identifiers) - - def update_dpp_by_id(self, dpp_id: str, patch: object) -> object: - current = self._read(dpp_id) - if "passportMetadata" in patch: - raise DppHttpClientError("invalid patch", 400, "{}") - characteristics = current.characteristics.with_updates( - productName=patch["characteristics"]["productName"] - ) - self.current = current.with_updates(characteristics=characteristics) - return self.current - - def read_dpp_version_by_id_and_date(self, dpp_id: str, _at: datetime) -> object: - self._read(dpp_id) - self.history_reads += 1 - return self.original if self.history_reads == 1 else self.current - - def read_data_element(self, dpp_id: str, selector: str) -> str: - assert selector == "$.characteristics.productName" - return self._read(dpp_id).productName - - def update_data_element(self, dpp_id: str, selector: str, value: str) -> str: - assert selector == "$.characteristics.productName" - current = self._read(dpp_id) - self.current = current.with_updates( - characteristics=current.characteristics.with_updates(productName=value) - ) - return value - - def delete_dpp_by_id(self, dpp_id: str) -> SimpleNamespace: - self._read(dpp_id) - self.current = None - return SimpleNamespace() - - def close(self) -> None: - pass - - -class _StatefulRegistry: - def __init__(self, repository: _StatefulRepository, *, ready: object = True) -> None: - self.repository = repository - self.ready = ready - self.registration_calls = 0 - - def health_check(self) -> object: - return self.ready - - def post_new_dpp_to_registry(self, request: object) -> SimpleNamespace: - self.registration_calls += 1 - assert self.repository.current is not None - return SimpleNamespace(registrationId=f"registration-{request.digitalProductPassportId}") - - def close(self) -> None: - pass +from dpp_mock_services_demo.reporting import ScenarioResult, ScenarioStatus def _config() -> DemoConfig: return DemoConfig( repo_base_url="http://localhost:18080", registry_base_url="http://localhost:18081", - repo_image="example/repository@sha256:repo", - registry_image="example/registry@sha256:registry", + repo_image="example/repository:current", + registry_image="example/registry:current", env_file=Path(__file__), - startup_timeout_seconds=2.0, + startup_timeout_seconds=0.1, legacy=False, ) -def test_integration_journey_has_stable_connected_steps() -> None: - identity = DemoIdentity.from_run_id(UUID("12345678-1234-5678-9234-567812345678")) - report = run_integration_scenarios(identity, config=None, repository=None, registry=None) - - assert tuple(step.step_id for step in report.steps) == tuple( - f"INT-{index:02d}" for index in range(1, 15) - ) - assert all( - step.purpose and step.input and step.operation and step.explanation for step in report.steps - ) - assert all(step.why_it_matters for step in report.steps) - assert all(step.status == "BLOCKED" for step in report.steps) - assert all(step.service["name"] == "not contacted" for step in report.steps) - assert all(step.response == {"result": "not attempted"} for step in report.steps) - assert all("blocked_by" in step.persistence_proof for step in report.steps) - - -def test_integration_journey_runs_all_public_operations_and_cleans_repository() -> None: - identity = DemoIdentity.from_run_id(UUID("12345678-1234-5678-9234-567812345678")) - repository = _StatefulRepository() - report = run_integration_scenarios( - identity, - _config(), - repository=repository, - registry=_StatefulRegistry(repository), - ) - - assert tuple(step.step_id for step in report.steps) == tuple( - f"INT-{index:02d}" for index in range(1, 15) - ) - assert [step.status for step in report.steps] == ["PASS"] * 12 + [ - "EXPECTED_ERROR", - "PASS", - ] - assert report.steps[8].response["historical_productName"] == "CIR4FUN Demo Chair" - assert ( - report.steps[8].response["updated_historical_productName"] - == report.steps[8].response["current_productName"] - ) - assert report.steps[8].response["current_productName"] != "CIR4FUN Demo Chair" - assert "created_at_utc" in report.steps[8].input - assert "updated_at_utc" in report.steps[8].input - assert "current_lookup_at_utc" in report.steps[8].input - assert report.steps[10].persistence_proof["read_back_value"].endswith("fine") - assert report.steps[6].response["dpp_ids"] == [str(identity.dpp_id)] - assert report.steps[6].persistence_proof["includes_created_dpp"] is True - assert report.steps[4].persistence_proof["read_back_dpp_id_matches_created"] is True - assert report.steps[5].persistence_proof["resolved_dpp_id_matches_created"] is True - assert report.steps[8].persistence_proof["created_historical_differs_from_current"] is True - assert report.steps[8].persistence_proof["updated_historical_matches_current"] is True - assert report.steps[9].persistence_proof["read_value_matches_full_patch"] is True - assert ( - report.steps[7].persistence_proof["read_back_productName"] - == report.steps[7].response["returned_productName"] - ) - assert report.steps[9].response["value"] == report.steps[7].response["returned_productName"] - assert ( - report.steps[10].persistence_proof["full_dpp_value"] - == report.steps[10].persistence_proof["read_back_value"] - ) - assert isinstance(report.context, IntegrationRunContext) - assert report.context.run_id == identity.run_id - assert report.context.dpp_id == identity.dpp_id - assert report.context.product_id == identity.product_id - assert report.context.initial_dpp.productName == "CIR4FUN Demo Chair" - assert report.context.updated_dpp is not None - assert report.context.updated_dpp.productName.endswith("fine") - assert report.context.repository_created is True - assert report.context.registration_id.startswith("registration-") - assert report.context.repository_cleanup == "deleted" - assert repository.current is None +class _Repository: + def __init__(self, *, fail_delete: bool = False) -> None: + self.payload = None + self.fail_delete = fail_delete + self.closed = False + def create_dpp(self, dpp, _representation=None): + self.payload = dpp + return CreateDppResponse(digital_product_passport_id=dpp["digitalProductPassportId"]) -def test_integration_stops_when_public_health_check_returns_false() -> None: - identity = DemoIdentity.from_run_id(UUID("12345678-1234-5678-9234-567812345678")) - repository = _StatefulRepository(ready=False) - report = run_integration_scenarios( - identity, - _config(), - repository=repository, - registry=_StatefulRegistry(_StatefulRepository()), - ) + def read_dpp_by_id(self, dpp_id, _representation=None): + if self.payload is None or self.payload["digitalProductPassportId"] != dpp_id: + raise DppApiClientError( + "missing", 404, DppStatusCode.ClientErrorResourceNotFound, None, "{}" + ) + return DppApiResponse(status_code=DppStatusCode.Success, payload=self.payload) - assert report.steps[0].status == "FAIL" - assert "health_check returned False" in report.steps[0].response["error"] - assert all(step.status == "BLOCKED" for step in report.steps[1:]) - assert repository.create_calls == 0 - assert integration_payload(report)["cleanup"]["repository"] == "not_created" + def delete_dpp_by_id(self, _dpp_id): + if self.fail_delete: + raise RuntimeError("delete unavailable") + self.payload = None + def close(self): + self.closed = True -def test_integration_stops_when_registry_health_check_returns_false() -> None: - identity = DemoIdentity.from_run_id(UUID("12345678-1234-5678-9234-567812345678")) - repository = _StatefulRepository() - registry = _StatefulRegistry(repository, ready=False) - report = run_integration_scenarios( - identity, _config(), repository=repository, registry=registry - ) +class _Registry: + def __init__(self) -> None: + self.request = None + self.closed = False - assert report.steps[0].status == "PASS" - assert report.steps[1].status == "FAIL" - assert "health_check returned False" in report.steps[1].response["error"] - assert all(step.status == "BLOCKED" for step in report.steps[2:]) - assert repository.create_calls == 0 - assert registry.registration_calls == 0 + def post_new_dpp_to_registry(self, request): + self.request = request + return RegisterDppResponse(registration_id="registration-1") + def close(self): + self.closed = True -def test_integration_blocks_after_timeout_connection_refusal_or_malformed_health() -> None: - identity = DemoIdentity.from_run_id(UUID("12345678-1234-5678-9234-567812345678")) - class _RaisingRepository(_StatefulRepository): - def __init__(self, error: Exception) -> None: - super().__init__() - self.error = error - - def health_check(self) -> object: - raise self.error - - for error, label in ( - (TimeoutError("health request timed out"), "TimeoutError"), - (ConnectionRefusedError("connection refused"), "ConnectionRefusedError"), - ): - repository = _RaisingRepository(error) - report = run_integration_scenarios( - identity, _config(), repository=repository, registry=_StatefulRegistry(repository) - ) - assert report.steps[0].status == "FAIL" - assert label in report.steps[0].response["error"] - assert all(step.status == "BLOCKED" for step in report.steps[1:]) - assert repository.create_calls == 0 - - malformed = _StatefulRepository(ready="UP") - report = run_integration_scenarios( - identity, _config(), repository=malformed, registry=_StatefulRegistry(malformed) +def _ready(**kwargs): + return ScenarioResult( + kwargs["scenario_id"], + kwargs["name"], + "LIVE_MOCK_IMAGE", + ScenarioStatus.PASSED, + 0.0, + "ready", + f"GET {kwargs['endpoint']}/health returned HTTP 200", ) - assert report.steps[0].status == "FAIL" - assert "must return True" in report.steps[0].response["error"] - assert malformed.create_calls == 0 -def test_integration_blocks_when_registry_readiness_is_not_a_true_response() -> None: +def test_unconfigured_run_is_bounded_and_makes_no_service_calls() -> None: identity = DemoIdentity.from_run_id(UUID("12345678-1234-5678-9234-567812345678")) - - class _RaisingRegistry(_StatefulRegistry): - def __init__(self, repository: _StatefulRepository, error: Exception) -> None: - super().__init__(repository) - self.error = error - - def health_check(self) -> object: - raise self.error - - for error, label in ( - (TimeoutError("registry health request timed out"), "TimeoutError"), - (ConnectionRefusedError("registry connection refused"), "ConnectionRefusedError"), - ): - repository = _StatefulRepository() - report = run_integration_scenarios( - identity, _config(), repository=repository, registry=_RaisingRegistry(repository, error) - ) - assert [step.status for step in report.steps[:2]] == ["PASS", "FAIL"] - assert label in report.steps[1].response["error"] - assert all(step.status == "BLOCKED" for step in report.steps[2:]) - assert repository.create_calls == 0 - - repository = _StatefulRepository() - malformed = _StatefulRegistry(repository, ready={"status": "UP"}) - report = run_integration_scenarios( - identity, _config(), repository=repository, registry=malformed + report = run_integration_scenarios(identity, config=None) + assert tuple(step.step_id for step in report.steps) == tuple( + f"INT-{i:02d}" for i in range(1, 9) ) - assert report.steps[1].status == "FAIL" - assert "must return True" in report.steps[1].response["error"] - assert repository.create_calls == 0 + assert all(step.status == "BLOCKED" for step in report.steps) + assert all(step.service["name"] == "not contacted" for step in report.steps) -def test_integration_history_does_not_claim_distinct_versions_without_evidence() -> None: - class _NoHistoryRepository(_StatefulRepository): - def read_dpp_version_by_id_and_date(self, dpp_id: str, _at: datetime) -> object: - return self._read(dpp_id) +def test_connected_flow_uses_json_clients_and_cleans_created_dpp(monkeypatch) -> None: + import dpp_mock_services_demo.integration_scenarios as module + monkeypatch.setattr(module, "wait_for_health", _ready) identity = DemoIdentity.from_run_id(UUID("12345678-1234-5678-9234-567812345678")) - repository = _NoHistoryRepository() - report = run_integration_scenarios( - identity, - _config(), - repository=repository, - registry=_StatefulRegistry(repository), - ) + repository = _Repository() + registry = _Registry() + report = run_integration_scenarios(identity, _config(), repository, registry) - assert report.steps[8].status == "FAIL" - assert ( - "historical lookup did not return the created version" in report.steps[8].response["error"] + assert tuple(step.step_id for step in report.steps) == tuple( + f"INT-{i:02d}" for i in range(1, 9) ) - - -def test_integration_output_identifies_images_stages_and_cleanup() -> None: - identity = DemoIdentity.from_run_id(UUID("12345678-1234-5678-9234-567812345678")) - repository = _StatefulRepository() - report = run_integration_scenarios( - identity, - _config(), - repository=repository, - registry=_StatefulRegistry(repository), + assert [step.status for step in report.steps] == [ + "PASS", + "PASS", + "PASS", + "PASS", + "PASS", + "PASS", + "EXPECTED_ERROR", + "PASS", + ] + assert report.context.repository_created is True + assert report.context.repository_cleanup == "deleted" + assert report.context.registration_id == "registration-1" + assert repository.payload is None + assert registry.request.model_dump(by_alias=True)["digitalProductPassportId"] == str( + identity.dpp_id ) + assert not repository.closed and not registry.closed - payload = integration_payload(report) - text = render_integration_text(report) - - assert payload["services"]["repository"]["configured_image"].endswith("sha256:repo") - assert payload["services"]["registry"]["configured_image"].endswith("sha256:registry") - assert "Environment and readiness" in text - assert text.count("Repository creation and reads") == 1 - assert text.count("Updates and history\n-------------------") == 1 - local_section = text.split("[INT-03]", maxsplit=1)[1].split("[INT-04]", maxsplit=1)[0] - assert "Service interaction" in local_section - assert "No external service call" in local_section - assert "Cleanup report:" in text - assert "repository=deleted; post_delete=HTTP 404" in text - assert ( - "registry=not supported by public SDK; state=registered_retained_no_public_cleanup" in text - ) - assert payload["cleanup"] == { - "repository": "deleted", - "public_sdk_deleted": [f"repository_dpp:{identity.dpp_id}"], - "post_delete_result": "HTTP 404", - "registry": "not_supported_public_api", - "registry_state": "registered_retained_no_public_cleanup", - "docker_project_cleanup": "separate operator responsibility", - "warnings": [], - } - - -def test_integration_reports_failed_repository_cleanup_without_claiming_delete() -> None: - class _DeleteFailingRepository(_StatefulRepository): - def delete_dpp_by_id(self, dpp_id: str) -> SimpleNamespace: - self._read(dpp_id) - raise DppHttpClientError("delete failed", 503, "{}") +def test_cleanup_failure_is_reported_without_claiming_success(monkeypatch) -> None: + import dpp_mock_services_demo.integration_scenarios as module + + monkeypatch.setattr(module, "wait_for_health", _ready) identity = DemoIdentity.from_run_id(UUID("12345678-1234-5678-9234-567812345678")) - repository = _DeleteFailingRepository() report = run_integration_scenarios( - identity, - _config(), - repository=repository, - registry=_StatefulRegistry(repository), + identity, _config(), _Repository(fail_delete=True), _Registry() ) - - payload = integration_payload(report) assert report.context.repository_cleanup == "delete_failed" - assert report.context.post_delete_result == "not_attempted" - assert payload["cleanup"]["public_sdk_deleted"] == [] - assert payload["cleanup"]["repository"] == "delete_failed" - assert payload["cleanup"]["warnings"] == [ - "repository cleanup failed: DppHttpClientError: delete failed" - ] - - -def test_integration_payload_distinguishes_runtime_identity_from_configured_reference() -> None: - identity = DemoIdentity.from_run_id(UUID("12345678-1234-5678-9234-567812345678")) - repository = _StatefulRepository() - image_identity = RuntimeImageIdentity( - repo_runtime_digest="sha256:" + "c" * 64, - registry_runtime_digest="sha256:" + "d" * 64, + assert report.cleanup_warnings == ( + "repository cleanup failed: RuntimeError: delete unavailable", ) + assert integration_payload(report)["cleanup"]["public_sdk_deleted"] == [] - report = run_integration_scenarios( - identity, - _config(), - repository=repository, - registry=_StatefulRegistry(repository), - image_identity=image_identity, - ) - payload = integration_payload(report) - assert payload["services"]["repository"] == { - "endpoint": "http://localhost:18080", - "configured_image": "example/repository@sha256:repo", - "runtime_digest": "sha256:" + "c" * 64, - } - assert payload["services"]["registry"]["runtime_digest"] == "sha256:" + "d" * 64 +def test_report_keeps_machine_readable_json_and_interaction_evidence(monkeypatch) -> None: + import dpp_mock_services_demo.integration_scenarios as module - -def test_integration_output_explains_unavailable_runtime_identity() -> None: + monkeypatch.setattr(module, "wait_for_health", _ready) identity = DemoIdentity.from_run_id(UUID("12345678-1234-5678-9234-567812345678")) - repository = _StatefulRepository() - report = run_integration_scenarios( - identity, - _config(), - repository=repository, - registry=_StatefulRegistry(repository), - image_identity_error=( - "ImageInspectionError: configured image does not match the running container" - ), - ) - + report = run_integration_scenarios(identity, _config(), _Repository(), _Registry()) payload = integration_payload(report) - rendered = render_integration_text(report) + json.dumps(payload, allow_nan=False) + assert len(payload["steps"]) == 8 + assert all("interaction" in step["service"] for step in payload["steps"]) + assert "DppRepoClient.create_dpp(json_value, FULL)" in render_integration_text(report) + assert "registry deregistration is not a supported operation" in render_integration_text(report) + assert isinstance(report.context, IntegrationRunContext) - assert payload["runtime_identity"]["status"] == "NOT_CAPTURED" - assert "configured image does not match the running container" in rendered +def test_readiness_failure_blocks_requests_and_cleanup(monkeypatch) -> None: + import dpp_mock_services_demo.integration_scenarios as module -def test_integration_report_has_complete_bounded_teaching_evidence_in_text_and_json() -> None: - identity = DemoIdentity.from_run_id(UUID("12345678-1234-5678-9234-567812345678")) - repository = _StatefulRepository() - report = run_integration_scenarios( - identity, - _config(), - repository=repository, - registry=_StatefulRegistry(repository), - ) + def failed(**kwargs): + if kwargs["scenario_id"] == "INT-01": + return ScenarioResult( + "INT-01", + "Repository", + "LIVE_MOCK_IMAGE", + ScenarioStatus.FAILED, + 0.0, + "failed", + "HTTP 503", + ) + return _ready(**kwargs) - payload = integration_payload(report) - rendered = render_integration_text(report) - expected_fields = { - "step_id", - "stage", - "title", - "status", - "evidence_class", - "purpose", - "input", - "operation", - "service", - "response", - "persistence_proof", - "explanation", - "why_it_matters", - "cleanup_effect", - "duration_seconds", - } - assert all(set(step) == expected_fields for step in payload["steps"]) - assert {step["status"] for step in payload["steps"]} == {"PASS", "EXPECTED_ERROR"} - assert all(step["input"] and step["operation"] and step["service"] for step in payload["steps"]) - assert all("interaction" in step["service"] for step in payload["steps"]) - assert all(step["response"] and step["persistence_proof"] for step in payload["steps"]) - assert all(step["explanation"] and step["why_it_matters"] for step in payload["steps"]) - assert set(payload["summary"]) == { - "total", - "passed", - "expected_error", - "blocked", - "skipped", - "fail", - "steps_completed", - "expected_errors_demonstrated", - "blocked_steps", - "skipped_steps", - "unexpected_failures", - } - assert payload["strict_verification"] == {"status": "NOT_RUN", "next_command": "verify"} - json.dumps(payload, allow_nan=False, sort_keys=True) - assert "0x" not in json.dumps(payload, sort_keys=True) - for heading in ( - "Environment and readiness", - "Local DPP preparation", - "Repository creation and reads", - "Updates and history", - "Fine-grained access", - "Registry interaction", - "Expected errors", - "Cleanup", - ): - assert heading in rendered - assert rendered.count("Service interaction") == 14 - assert "Strict verification: NOT_RUN (separate command)" in rendered - - -def test_demo_profile_executes_only_the_curated_connected_journey() -> None: + monkeypatch.setattr(module, "wait_for_health", failed) identity = DemoIdentity.from_run_id(UUID("12345678-1234-5678-9234-567812345678")) - repository = _StatefulRepository() - report = run_integration_scenarios( - identity, - _config(), - repository=repository, - registry=_StatefulRegistry(repository), - profile="demo", - ) - - assert report.profile == "demo" - assert [step.step_id for step in report.steps] == [ - "INT-01", - "INT-02", - "INT-03", - "INT-04", - "INT-05", - "INT-08", - "INT-12", - "INT-14", - ] - assert integration_payload(report)["verdict"] == "LIVE_DEMONSTRATION_PASSED" + repository = _Repository() + report = run_integration_scenarios(identity, _config(), repository, _Registry()) + assert report.steps[0].status == "FAIL" + assert repository.payload is None + assert all(step.status != "PASS" for step in report.steps[1:]) diff --git a/examples/mock-services-demo/tests/test_manage_services_script.py b/examples/mock-services-demo/tests/test_manage_services_script.py index cf05013..69d521d 100644 --- a/examples/mock-services-demo/tests/test_manage_services_script.py +++ b/examples/mock-services-demo/tests/test_manage_services_script.py @@ -8,6 +8,7 @@ def test_lifecycle_script_is_project_scoped_and_guards_volume_deletion() -> None: script = (PROJECT_ROOT / "manage-mock-services.ps1").read_text(encoding="utf-8") guide = (PROJECT_ROOT / "README.md").read_text(encoding="utf-8") + advanced = (PROJECT_ROOT / "ADVANCED_OPERATIONS.md").read_text(encoding="utf-8") example = (PROJECT_ROOT / ".env.example").read_text(encoding="utf-8") assert '[ValidateSet("Start", "Status", "Logs", "Stop", "Delete")]' in script @@ -25,11 +26,13 @@ def test_lifecycle_script_is_project_scoped_and_guards_volume_deletion() -> None assert "-ConfirmDelete" in script assert "down --volumes --remove-orphans" in script assert 'Copy-Item (Join-Path $demoDir ".env.example") $envFile' in guide - assert "project-prefixed container names" in guide - assert "Docker Engine or Docker Desktop must be running" in guide - assert "Optional PowerShell convenience wrapper" in guide - assert guide.index("docker compose --env-file $envFile -f $composeFile -p $project pull") < ( - guide.index('& (Join-Path $demoDir "manage-mock-services.ps1") -Action Start') + assert "project-scoped" in guide + assert "docker compose version" in guide + assert "Optional PowerShell lifecycle helper" not in guide + assert "Optional PowerShell lifecycle helper" in advanced + pull_command = "docker compose --env-file $envFile -f $composeFile -p $project pull" + assert advanced.index(pull_command) < ( + advanced.index('& (Join-Path $demoDir "manage-mock-services.ps1") -Action Start') ) assert "COMPOSE_PROJECT_NAME=" in example assert "MOCK_REPO_PORT=" in example diff --git a/examples/mock-services-demo/tests/test_mock_services_integration.py b/examples/mock-services-demo/tests/test_mock_services_integration.py index e5e0c0a..3213480 100644 --- a/examples/mock-services-demo/tests/test_mock_services_integration.py +++ b/examples/mock-services-demo/tests/test_mock_services_integration.py @@ -4,18 +4,17 @@ import pytest -from dpp_mock_services_demo.config import load_config -from dpp_mock_services_demo.fixtures import DemoIdentity -from dpp_mock_services_demo.registry_scenarios import run_registry_scenarios -from dpp_mock_services_demo.reporting import ScenarioStatus -from dpp_mock_services_demo.repository_scenarios import run_repository_scenarios - pytestmark = pytest.mark.integration def test_repository_scenarios_against_published_mock_image( require_mock_services: None, ) -> None: + from dpp_mock_services_demo.config import load_config + from dpp_mock_services_demo.fixtures import DemoIdentity + from dpp_mock_services_demo.reporting import ScenarioStatus + from dpp_mock_services_demo.repository_scenarios import run_repository_scenarios + run = run_repository_scenarios(load_config(), DemoIdentity.from_run_id(uuid4())) failures = [result for result in run.results if result.status is not ScenarioStatus.PASSED] @@ -26,6 +25,11 @@ def test_repository_scenarios_against_published_mock_image( def test_registry_scenarios_against_published_mock_image( require_mock_services: None, ) -> None: + from dpp_mock_services_demo.config import load_config + from dpp_mock_services_demo.fixtures import DemoIdentity + from dpp_mock_services_demo.registry_scenarios import run_registry_scenarios + from dpp_mock_services_demo.reporting import ScenarioStatus + run = run_registry_scenarios(load_config(), DemoIdentity.from_run_id(uuid4())) failures = [result for result in run.results if result.status is not ScenarioStatus.PASSED] diff --git a/examples/mock-services-demo/tests/test_registry_scenarios.py b/examples/mock-services-demo/tests/test_registry_scenarios.py index 5c5a56f..e657970 100644 --- a/examples/mock-services-demo/tests/test_registry_scenarios.py +++ b/examples/mock-services-demo/tests/test_registry_scenarios.py @@ -3,11 +3,8 @@ from pathlib import Path from uuid import UUID -from dpp_sdk import Dpp4Fun from dpp_sdk.clients import ( - CreateDppResponse, - DeleteDppResponse, - DppHttpClientError, + DppApiClientError, DppStatusCode, RegisterDppRequest, RegisterDppResponse, @@ -16,51 +13,26 @@ from dpp_mock_services_demo.config import DemoConfig from dpp_mock_services_demo.fixtures import DemoIdentity from dpp_mock_services_demo.registry_scenarios import run_registry_scenarios -from dpp_mock_services_demo.reporting import ScenarioStatus +from dpp_mock_services_demo.reporting import ScenarioResult, ScenarioStatus -class FakeRepo: +class _Registry: def __init__(self) -> None: - self.active: set[str] = set() - - def health_check(self) -> bool: - return True - - def create_dpp(self, dpp: Dpp4Fun) -> CreateDppResponse: - self.active.add(dpp.dpp_id) - return CreateDppResponse(dppId=dpp.dpp_id) - - def delete_dpp_by_id(self, dpp_id: str) -> DeleteDppResponse: - self.active.remove(dpp_id) - return DeleteDppResponse(statusCode=DppStatusCode.SuccessNoContent) - - def close(self) -> None: - pass - - -class FakeRegistry: - def __init__(self, repo: FakeRepo) -> None: - self.repo = repo - self.registered: set[str] = set() - - def health_check(self) -> bool: - return True + self.calls = 0 + self.closed = False def post_new_dpp_to_registry(self, request: RegisterDppRequest) -> RegisterDppResponse: - if not request.uniqueProductIdentifier: - raise DppHttpClientError("invalid request", 400, "{}") - if request.dppApiEndpoint == "http://127.0.0.1:1": - raise DppHttpClientError("unavailable repository", 502, "{}") - dpp_id = request.digitalProductPassportId - if dpp_id not in self.repo.active: - raise DppHttpClientError("missing repository DPP", 404, "{}") - if dpp_id in self.registered: - raise DppHttpClientError("duplicate registration", 409, "{}") - self.registered.add(dpp_id) - return RegisterDppResponse(registrationId=f"registration-{dpp_id}") + self.calls += 1 + if self.calls == 2: + raise DppApiClientError( + "duplicate", 409, DppStatusCode.ClientResourceConflict, None, "{}" + ) + if not request.unique_product_identifier: + raise DppApiClientError("invalid", 400, DppStatusCode.ClientErrorBadRequest, None, "{}") + return RegisterDppResponse(registration_id="registration-1") def close(self) -> None: - pass + self.closed = True def _config() -> DemoConfig: @@ -70,44 +42,69 @@ def _config() -> DemoConfig: repo_image="repo", registry_image="registry", env_file=Path(__file__), - startup_timeout_seconds=2.0, + startup_timeout_seconds=0.1, legacy=False, ) -def test_registry_runner_covers_reg_01_through_reg_07_without_internal_routes() -> None: - identity = DemoIdentity.from_run_id(UUID("12345678-1234-5678-9234-567812345678")) - repo = FakeRepo() - registry = FakeRegistry(repo) - - run = run_registry_scenarios( - _config(), - identity, - repo_client=repo, - registry_client=registry, +def test_registry_runner_has_no_repository_contact_assumptions(monkeypatch) -> None: + import dpp_mock_services_demo.registry_scenarios as module + + monkeypatch.setattr( + module, + "wait_for_health", + lambda **kwargs: ScenarioResult( + kwargs["scenario_id"], + kwargs["name"], + "test", + ScenarioStatus.PASSED, + 0.0, + "ready", + "HTTP 200", + ), ) + registry = _Registry() + identity = DemoIdentity.from_run_id(UUID("12345678-1234-5678-9234-567812345678")) + run = run_registry_scenarios(_config(), identity, registry_client=registry) - assert tuple(result.scenario_id for result in run.results) == tuple( - f"REG-{index:02d}" for index in range(1, 8) + assert tuple(item.scenario_id for item in run.results) == ( + "REG-01", + "REG-02", + "REG-03", + "REG-04", ) - assert all(result.status is ScenarioStatus.PASSED for result in run.results) + assert [item.status for item in run.results] == [ + ScenarioStatus.PASSED, + ScenarioStatus.PASSED, + ScenarioStatus.PASSED, + ScenarioStatus.PASSED, + ] + assert registry.calls == 3 + assert not registry.closed assert run.cleanup_warnings == () - assert repo.active == set() - -def test_registry_cleanup_failure_is_reported_as_warning() -> None: - identity = DemoIdentity.from_run_id(UUID("12345678-1234-5678-9234-567812345678")) - - class FailingCleanupRepo(FakeRepo): - def delete_dpp_by_id(self, dpp_id: str) -> DeleteDppResponse: - raise DppHttpClientError("cleanup failed", 500, "{}") - repo = FailingCleanupRepo() - run = run_registry_scenarios( - _config(), - identity, - repo_client=repo, - registry_client=FakeRegistry(repo), +def test_registry_client_created_by_runner_is_closed(monkeypatch) -> None: + import dpp_mock_services_demo.registry_scenarios as module + + monkeypatch.setattr( + module, + "wait_for_health", + lambda **kwargs: ScenarioResult( + kwargs["scenario_id"], + kwargs["name"], + "test", + ScenarioStatus.FAILED, + 0.0, + "not ready", + "HTTP 503", + ), ) - assert run.cleanup_warnings + # Readiness fails before registration. The runner still owns and closes its client. + class _Client(_Registry): + pass + + monkeypatch.setattr(module, "DppRegistryClient", lambda _url: _Client()) + result = run_registry_scenarios(_config(), DemoIdentity.from_run_id(UUID(int=1))) + assert all(item.status is ScenarioStatus.FAILED for item in result.results) diff --git a/examples/mock-services-demo/tests/test_reporting.py b/examples/mock-services-demo/tests/test_reporting.py index b4ce398..cb2bd51 100644 --- a/examples/mock-services-demo/tests/test_reporting.py +++ b/examples/mock-services-demo/tests/test_reporting.py @@ -3,6 +3,8 @@ import json from uuid import UUID +from dpp_sdk import __version__ as sdk_version + from dpp_mock_services_demo.reporting import ( DemoReport, InteroperabilityVerdict, @@ -45,7 +47,7 @@ def _report(status: ScenarioStatus = ScenarioStatus.PASSED) -> DemoReport: ), summary="SDK capability demonstration completed", partial=False, - sdk_version="0.4.0", + sdk_version=sdk_version, sdk_location="/installed/site-packages/dpp_sdk/__init__.py", repo_image="repo@example", registry_image="registry@example", @@ -73,7 +75,7 @@ def test_json_report_is_machine_readable_and_complete() -> None: assert payload["mode"] == "sdk" assert payload["run_id"] == "12345678-1234-5678-9234-567812345678" - assert payload["sdk_version"] == "0.4.0" + assert payload["sdk_version"] == sdk_version assert result["scenario_id"] == "SDK-01" assert result["summary"] == "Complete fixture passed" assert result["teaching"]["operation"]["public_api"] == "Dpp4Fun" diff --git a/examples/mock-services-demo/tests/test_repository_scenarios.py b/examples/mock-services-demo/tests/test_repository_scenarios.py index c024dfb..eacbb64 100644 --- a/examples/mock-services-demo/tests/test_repository_scenarios.py +++ b/examples/mock-services-demo/tests/test_repository_scenarios.py @@ -1,20 +1,22 @@ from __future__ import annotations from datetime import datetime -from typing import Any +from pathlib import Path from uuid import UUID -from dpp_sdk import Dpp4Fun +import httpx from dpp_sdk.clients import ( CreateDppResponse, - DeleteDppResponse, - DppHttpClientError, + DppApiClientError, + DppApiResponse, + DppMappingClientError, + DppRepresentation, DppStatusCode, ReadDppIdsResponse, ) from dpp_mock_services_demo.config import DemoConfig -from dpp_mock_services_demo.fixtures import DemoIdentity, build_complete_fixture +from dpp_mock_services_demo.fixtures import DemoIdentity from dpp_mock_services_demo.reporting import ScenarioStatus from dpp_mock_services_demo.repository_scenarios import ( run_repository_scenarios, @@ -22,92 +24,71 @@ ) -class FakeRepositoryClient: - def __init__(self, original: Dpp4Fun, *, health: list[bool] | None = None) -> None: - self.original = original - self.current = original +class _JsonRepository: + def __init__(self) -> None: + self.payload = None self.active = False - self.health = list(health or [True]) - self.history_reads = 0 + self.element_reads = [] + self.element_updates = [] - def health_check(self) -> bool: - return self.health.pop(0) if self.health else True - - def create_dpp(self, dpp: Dpp4Fun) -> CreateDppResponse: + def create_dpp(self, value, _representation=None): + if value is None: + raise DppMappingClientError("null JSON") if self.active: - raise DppHttpClientError("conflict", 409, "{}") + raise DppApiClientError( + "duplicate", 409, DppStatusCode.ClientResourceConflict, None, "{}" + ) + self.payload = value self.active = True - self.original = dpp - self.current = dpp - return CreateDppResponse(dppId=dpp.dpp_id) - - def read_dpp_by_id(self, dpp_id: str) -> Dpp4Fun: - if not self.active or dpp_id != self.current.dpp_id: - raise DppHttpClientError("missing", 404, "{}") - return self.current - - def read_compressed_dpp_by_id(self, dpp_id: str) -> dict[str, Any]: - self.read_dpp_by_id(dpp_id) - return {"dppId": dpp_id, "representation": "compressed"} - - def read_dpp_by_product_id(self, product_id: str) -> Dpp4Fun: - if not self.active or product_id != self.current.product_id: - raise DppHttpClientError("missing product", 404, "{}") - return self.current - - def read_dpp_version_by_id_and_date(self, dpp_id: str, at: datetime) -> Dpp4Fun: + return CreateDppResponse(digital_product_passport_id=value["digitalProductPassportId"]) + + def read_dpp_by_id(self, dpp_id, _representation=None): + if not self.active or self.payload["digitalProductPassportId"] != dpp_id: + raise DppApiClientError( + "missing", 404, DppStatusCode.ClientErrorResourceNotFound, None, "{}" + ) + return DppApiResponse(status_code=DppStatusCode.Success, payload=self.payload) + + def read_dpp_by_product_id(self, product_id, _representation=None): + if not self.active or self.payload["uniqueProductIdentifier"] != product_id: + raise DppApiClientError( + "missing", 404, DppStatusCode.ClientErrorResourceNotFound, None, "{}" + ) + return DppApiResponse(status_code=DppStatusCode.Success, payload=self.payload) + + def read_dpp_version_by_id_and_date(self, dpp_id, at: datetime, _representation=None): assert at.tzinfo is not None - self.read_dpp_by_id(dpp_id) - self.history_reads += 1 - return self.original if self.history_reads == 1 else self.current + return self.read_dpp_by_id(dpp_id) - def read_dpp_ids_by_product_ids( - self, product_ids: list[str], limit: int | None = None, cursor: str | None = None - ) -> ReadDppIdsResponse: + def read_dpp_ids_by_product_ids(self, product_ids, limit=None, cursor=None): assert limit == 10 and cursor == "0" - identifiers = [self.current.dpp_id] if self.current.product_id in product_ids else [] - return ReadDppIdsResponse(dppIdentifiers=identifiers, nextCursor=None) - - def update_dpp_by_id(self, dpp_id: str, patch: Any) -> Dpp4Fun: + ids = ( + [self.payload["digitalProductPassportId"]] + if self.active and self.payload["uniqueProductIdentifier"] in product_ids + else [] + ) + return ReadDppIdsResponse(dpp_identifiers=ids) + + def update_dpp_by_id(self, dpp_id, patch, _representation=None): self.read_dpp_by_id(dpp_id) - if "passportMetadata" in patch: - raise DppHttpClientError("invalid patch", 400, "{}") - product_name = patch["characteristics"]["productName"] - characteristics = self.current.characteristics.with_updates(productName=product_name) - self.current = self.current.with_updates(characteristics=characteristics) - return self.current - - def read_data_element(self, dpp_id: str, path: str) -> Any: - self.read_dpp_by_id(dpp_id) - errors = { - "characteristics.productName": 400, - "$.characteristics.missing": 404, - "$.billOfMaterials.materials[*]": 501, - } - if path in errors: - raise DppHttpClientError("fine read error", errors[path], "{}") - if path == "$.characteristics.productName": - return self.current.productName - if path == "$.billOfMaterials.materials[0].name": - assert self.current.billOfMaterials is not None - return self.current.billOfMaterials.materials[0].name - raise AssertionError(path) - - def update_data_element(self, dpp_id: str, path: str, payload: Any) -> Any: + return DppApiResponse(status_code=DppStatusCode.Success, payload=self.payload) + + def read_data_element(self, dpp_id, path, _representation=None): self.read_dpp_by_id(dpp_id) - if path == "$": - raise DppHttpClientError("root replacement", 400, "{}") - assert path == "$.characteristics.productName" - characteristics = self.current.characteristics.with_updates(productName=payload) - self.current = self.current.with_updates(characteristics=characteristics) - return payload - - def delete_dpp_by_id(self, dpp_id: str) -> DeleteDppResponse: + if not path.strip(): + raise ValueError("blank element path") + self.element_reads.append((path, _representation)) + return DppApiResponse(status_code=DppStatusCode.Success, payload="blue") + + def update_data_element(self, dpp_id, path, data, _representation=None): self.read_dpp_by_id(dpp_id) + self.element_updates.append((path, data, _representation)) + return DppApiResponse(status_code=DppStatusCode.Success, payload=data) + + def delete_dpp_by_id(self, _dpp_id): self.active = False - return DeleteDppResponse(statusCode=DppStatusCode.SuccessNoContent) - def close(self) -> None: + def close(self): pass @@ -117,61 +98,93 @@ def _config() -> DemoConfig: registry_base_url="http://localhost:8081", repo_image="repo", registry_image="registry", - env_file=__file__, # type: ignore[arg-type] + env_file=Path(__file__), startup_timeout_seconds=2.0, legacy=False, ) -def test_repository_readiness_retries_functional_health_without_fixed_sleep() -> None: - identity = DemoIdentity.from_run_id(UUID("12345678-1234-5678-9234-567812345678")) - client = FakeRepositoryClient(build_complete_fixture(identity), health=[False, False, True]) - clock_values = iter((0.0, 0.1, 0.2, 0.3)) +def test_repository_readiness_probes_public_http_health_and_retries() -> None: + responses = iter((503, 200)) + http = httpx.Client( + transport=httpx.MockTransport(lambda _request: httpx.Response(next(responses))) + ) + clock_values = iter((0.0, 0.1, 0.2)) pauses: list[float] = [] - result = wait_for_repository( _config(), - client=client, + client=http, clock=lambda: next(clock_values), pause=pauses.append, interval_seconds=0.01, ) - assert result.status is ScenarioStatus.PASSED - assert pauses == [0.01, 0.01] + assert pauses == [0.01] + assert http.is_closed is False + http.close() -def test_repository_runner_covers_rep_01_through_rep_15_and_preserves_original() -> None: - identity = DemoIdentity.from_run_id(UUID("12345678-1234-5678-9234-567812345678")) - original = build_complete_fixture(identity) - client = FakeRepositoryClient(original) +def test_repository_runner_uses_json_api_operations_and_cleans_up(monkeypatch) -> None: + import dpp_mock_services_demo.repository_scenarios as module + from dpp_mock_services_demo.reporting import ScenarioResult + monkeypatch.setattr( + module, + "wait_for_repository", + lambda *_args, **_kwargs: ScenarioResult( + "REP-01", "ready", "test", ScenarioStatus.PASSED, 0, "ready" + ), + ) + identity = DemoIdentity.from_run_id(UUID("12345678-1234-5678-9234-567812345678")) + client = _JsonRepository() run = run_repository_scenarios(_config(), identity, client=client) - assert tuple(result.scenario_id for result in run.results) == tuple( - f"REP-{index:02d}" for index in range(1, 16) + f"REP-{i:02d}" for i in range(1, 16) ) - assert all(result.status is ScenarioStatus.PASSED for result in run.results) - assert run.cleanup_warnings == () - assert original.productName == "CIR4FUN Demo Chair" + assert [item.status for item in run.results] == [ + ScenarioStatus.PASSED, + ScenarioStatus.PASSED, + ScenarioStatus.PASSED, + ScenarioStatus.PASSED, + ScenarioStatus.PASSED, + ScenarioStatus.PASSED, + ScenarioStatus.PASSED, + ScenarioStatus.PASSED, + ScenarioStatus.PASSED, + ScenarioStatus.PASSED, + ScenarioStatus.PASSED, + ScenarioStatus.EXPECTED_ERROR, + ScenarioStatus.EXPECTED_ERROR, + ScenarioStatus.EXPECTED_ERROR, + ScenarioStatus.EXPECTED_ERROR, + ] assert client.active is False + read_path, read_representation = client.element_reads[0] + update_path, update_data, update_representation = client.element_updates[0] + assert read_path == update_path + characteristics_index = next( + index + for index, element in enumerate(client.payload["elements"]) + if element["elementId"] == "characteristics" + ) + color_index = next( + index + for index, element in enumerate( + client.payload["elements"][characteristics_index]["elements"] + ) + if element["elementId"] == "color" + ) + assert read_path == f"$.elements[{characteristics_index}].elements[{color_index}]" + assert read_representation is DppRepresentation.FULL + assert update_representation is DppRepresentation.FULL + assert update_data["objectType"] == "SingleValuedDataElement" + assert update_data["elementId"] == "color" + assert update_data["value"] == "green" + assert run.cleanup_warnings == () -def test_cleanup_failure_is_a_warning_separate_from_scenario_failure() -> None: - identity = DemoIdentity.from_run_id(UUID("12345678-1234-5678-9234-567812345678")) - - class FailingDeleteClient(FakeRepositoryClient): - def delete_dpp_by_id(self, dpp_id: str) -> DeleteDppResponse: - raise DppHttpClientError("delete failed", 500, "{}") - - run = run_repository_scenarios( - _config(), - identity, - client=FailingDeleteClient(build_complete_fixture(identity)), - ) +def test_live_inventory_uses_full_representation_and_no_removed_codec_injection() -> None: + from dpp_mock_services_demo.repository_scenarios import _NAMES - assert any( - result.scenario_id == "REP-11" and result.status is ScenarioStatus.FAILED - for result in run.results - ) - assert run.cleanup_warnings + assert tuple(_NAMES) == tuple(f"REP-{index:02d}" for index in range(1, 16)) + assert _NAMES["REP-04"] == "Read explicit FULL representation" diff --git a/examples/mock-services-demo/tests/test_sdk_output.py b/examples/mock-services-demo/tests/test_sdk_output.py index 0b7ef2e..a5cf938 100644 --- a/examples/mock-services-demo/tests/test_sdk_output.py +++ b/examples/mock-services-demo/tests/test_sdk_output.py @@ -26,8 +26,8 @@ def test_sdk_text_summary_is_compact_and_keeps_statuses() -> None: text = render_text(report, summary=True) - assert "SDK-01 | Complete model construction" in text - assert "SDK-07 | Valid immutable updates" in text + assert "SDK-01 | Typed DPP4Fun validation" in text + assert "SDK-07 | Unknown extension preservation" in text assert "Purpose\n" not in text assert "expected_error=0" in text @@ -36,7 +36,7 @@ def test_sdk_detailed_text_keeps_an_unexpected_failure_visible() -> None: base = _report("sdk") failure = ScenarioResult( scenario_id="SDK-01", - name="Complete model construction", + name="Typed DPP4Fun validation", category="SDK_LOCAL", status=ScenarioStatus.FAILED, duration_seconds=0.0, @@ -53,7 +53,7 @@ def test_sdk_detailed_text_keeps_an_unexpected_failure_visible() -> None: text = render_text(report) - assert "[SDK-01] Complete model construction" in text + assert "[SDK-01] Typed DPP4Fun validation" in text assert "Status: FAILED" in text assert "RuntimeError: controlled failure" in text diff --git a/examples/mock-services-demo/tests/test_sdk_scenarios.py b/examples/mock-services-demo/tests/test_sdk_scenarios.py index 715ffd1..1072cea 100644 --- a/examples/mock-services-demo/tests/test_sdk_scenarios.py +++ b/examples/mock-services-demo/tests/test_sdk_scenarios.py @@ -5,10 +5,10 @@ from dpp_mock_services_demo.reporting import ScenarioStatus from dpp_mock_services_demo.sdk_scenarios import run_sdk_scenarios -EXPECTED_IDS = tuple(f"SDK-{index:02d}" for index in range(1, 18)) +EXPECTED_IDS = tuple(f"SDK-{index:02d}" for index in range(1, 13)) -def test_sdk_runner_executes_the_approved_capability_matrix() -> None: +def test_sdk_runner_executes_the_final_python_capability_examples() -> None: results = run_sdk_scenarios(UUID("12345678-1234-5678-9234-567812345678")) assert tuple(result.scenario_id for result in results) == EXPECTED_IDS @@ -16,88 +16,55 @@ def test_sdk_runner_executes_the_approved_capability_matrix() -> None: result.status in {ScenarioStatus.PASSED, ScenarioStatus.EXPECTED_ERROR} for result in results ) - assert all(result.category for result in results) - assert all(result.name for result in results) - assert all(result.summary for result in results) + assert all(result.category and result.name and result.summary for result in results) assert all(result.duration_seconds >= 0 for result in results) -def test_sdk_runner_can_select_the_small_public_offline_example() -> None: +def test_sdk_runner_keeps_the_short_offline_walkthrough_selectable() -> None: results = run_sdk_scenarios( UUID("12345678-1234-5678-9234-567812345678"), - scenario_ids=("SDK-01", "SDK-02", "SDK-03", "SDK-06", "SDK-07"), + scenario_ids=("SDK-01", "SDK-02", "SDK-03", "SDK-08", "SDK-12"), ) - assert tuple(result.scenario_id for result in results) == ( "SDK-01", "SDK-02", "SDK-03", - "SDK-06", - "SDK-07", + "SDK-08", + "SDK-12", ) def test_sdk_runner_captures_complete_structured_teaching_evidence() -> None: - results = run_sdk_scenarios(UUID("12345678-1234-5678-9234-567812345678")) - - for result in results: + for result in run_sdk_scenarios(UUID("12345678-1234-5678-9234-567812345678")): teaching = result.teaching assert teaching is not None - assert teaching.group - assert teaching.evidence_class - assert teaching.purpose + assert teaching.group and teaching.evidence_class and teaching.purpose assert teaching.input - assert teaching.operation["public_api"] - assert teaching.operation["display"] - assert teaching.expected_behavior - assert teaching.observed_result - assert teaching.explanation - assert teaching.why_it_matters - - -def test_expected_negative_contracts_are_reported_as_expected_errors() -> None: - results = { - result.scenario_id: result - for result in run_sdk_scenarios(UUID("12345678-1234-5678-9234-567812345678")) - } - - negative_scenarios = ( - "SDK-04", - "SDK-05", - "SDK-08", - "SDK-10", - "SDK-11", - "SDK-12", - "SDK-13", - "SDK-14", - "SDK-16", - ) - for scenario_id in negative_scenarios: - assert results[scenario_id].status is ScenarioStatus.EXPECTED_ERROR - assert results[scenario_id].details + assert teaching.operation["public_api"] and teaching.operation["display"] + assert teaching.expected_behavior and teaching.observed_result + assert teaching.explanation and teaching.why_it_matters + + +def test_invalid_client_request_is_reported_as_expected_error() -> None: + results = {item.scenario_id: item for item in run_sdk_scenarios()} + assert results["SDK-12"].status is ScenarioStatus.EXPECTED_ERROR + assert "DppMappingClientError" in results["SDK-12"].details -def test_sdk_runner_converts_an_uncontrolled_scenario_error_to_failed_result( - monkeypatch, -) -> None: +def test_sdk_runner_converts_uncontrolled_scenario_errors_to_failed_results(monkeypatch) -> None: import dpp_mock_services_demo.sdk_scenarios as scenarios - def fail(_context) -> str: + def fail(_identity, _passport) -> str: raise RuntimeError("controlled test failure") - failing_scenarios = [ - scenarios.SCENARIOS[0]._replace(run=fail), - *scenarios.SCENARIOS[1:], - ] - monkeypatch.setattr(scenarios, "SCENARIOS", failing_scenarios) - + failing = [scenarios.SCENARIOS[0]._replace(run=fail), *scenarios.SCENARIOS[1:]] + monkeypatch.setattr(scenarios, "SCENARIOS", failing) result = scenarios.run_sdk_scenarios()[0] - assert result.status is ScenarioStatus.FAILED assert "RuntimeError" in result.details -def test_sdk_runner_rejects_a_scenario_with_missing_teaching_metadata(monkeypatch) -> None: +def test_sdk_runner_rejects_missing_teaching_metadata(monkeypatch) -> None: import pytest import dpp_mock_services_demo.sdk_scenarios as scenarios @@ -107,6 +74,5 @@ def test_sdk_runner_rejects_a_scenario_with_missing_teaching_metadata(monkeypatc "SCENARIOS", [scenarios.SCENARIOS[0]._replace(explanation=""), *scenarios.SCENARIOS[1:]], ) - with pytest.raises(AssertionError, match="SDK-01 has incomplete teaching metadata"): scenarios.run_sdk_scenarios(UUID("12345678-1234-5678-9234-567812345678")) diff --git a/examples/mock-services-demo/tests/verification/test_live_contract_matrix.py b/examples/mock-services-demo/tests/verification/test_live_contract_matrix.py index 6b21fa5..68c3e71 100644 --- a/examples/mock-services-demo/tests/verification/test_live_contract_matrix.py +++ b/examples/mock-services-demo/tests/verification/test_live_contract_matrix.py @@ -21,7 +21,7 @@ def test_complete_repository_and_registry_live_contract_matrix() -> None: f"REP-{index:02d}" for index in range(1, 16) ) assert tuple(result.scenario_id for result in registry.results) == tuple( - f"REG-{index:02d}" for index in range(1, 8) + f"REG-{index:02d}" for index in range(1, 5) ) assert all(result.status is ScenarioStatus.PASSED for result in repository.results) assert all(result.status is ScenarioStatus.PASSED for result in registry.results) diff --git a/examples/mock-services-demo/tests/verification/test_maintainer.py b/examples/mock-services-demo/tests/verification/test_maintainer.py index 4bab622..e049e29 100644 --- a/examples/mock-services-demo/tests/verification/test_maintainer.py +++ b/examples/mock-services-demo/tests/verification/test_maintainer.py @@ -24,7 +24,7 @@ def test_maintainer_summary_is_one_line_and_omits_scenario_details( output = capsys.readouterr().out.strip() assert output.startswith("SDK:") assert "SDK-01" not in output - assert "total=17" in output + assert "total=12" in output def test_public_cli_help_keeps_legacy_modes_out_of_the_consumer_path() -> None: @@ -38,5 +38,5 @@ def test_public_cli_help_keeps_legacy_modes_out_of_the_consumer_path() -> None: def test_sdk_contract_mode_keeps_full_sdk_inventory() -> None: report = run_maintainer_mode("sdk-contracts") assert tuple(result.scenario_id for result in report.results) == tuple( - f"SDK-{index:02d}" for index in range(1, 18) + f"SDK-{index:02d}" for index in range(1, 13) ) diff --git a/examples/mock-services-demo/tests/verification/test_verification_inventory.py b/examples/mock-services-demo/tests/verification/test_verification_inventory.py index 0881f8b..3fc02b3 100644 --- a/examples/mock-services-demo/tests/verification/test_verification_inventory.py +++ b/examples/mock-services-demo/tests/verification/test_verification_inventory.py @@ -6,10 +6,10 @@ def test_maintained_verification_inventory_is_complete() -> None: assert tuple(scenario.scenario_id for scenario in SCENARIOS) == tuple( - f"SDK-{index:02d}" for index in range(1, 18) + f"SDK-{index:02d}" for index in range(1, 13) ) assert tuple(repository_scenarios._NAMES) == tuple(f"REP-{index:02d}" for index in range(1, 16)) - assert tuple(registry_scenarios._NAMES) == tuple(f"REG-{index:02d}" for index in range(1, 8)) + assert tuple(registry_scenarios._NAMES) == tuple(f"REG-{index:02d}" for index in range(1, 5)) controlled_ids = tuple( result.scenario_id for result in controlled_scenarios.run_controlled_scenarios() ) diff --git a/scripts/generate_dpp4fun_fields.py b/scripts/generate_dpp4fun_fields.py new file mode 100644 index 0000000..873331e --- /dev/null +++ b/scripts/generate_dpp4fun_fields.py @@ -0,0 +1,129 @@ +"""Generate docs/dpp4fun-fields.md from the bundled DPP4Fun semantic profile. + +Run from the repository root: ``python scripts/generate_dpp4fun_fields.py``. +A documentation test fails when the committed file differs from this output. +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +PROFILE = ROOT / "src" / "dpp_sdk" / "dpp4fun" / "semantic" / "dpp4fun-semantic-profile-v1.json" +OUTPUT = ROOT / "docs" / "dpp4fun-fields.md" + +KINDS = { + "DataElementCollection": "collection", + "SingleValuedDataElement": "single value", + "MultiValuedDataElement": "list", + "MultiLanguageDataElement": "multi-language text", +} + +HEADER = """\ +# DPP4Fun field reference + + + +This page lists every field of the DPP4Fun furniture profile that `dpp_sdk.dpp4fun` knows. +Use it to find the names you pass to the typed classes. + +How to read the tables: + +- **Field** is the path inside the aspect. Pass the first segment as a keyword to + `Aspect.create(...)` or as the name to `with_field()`, `field()`, and `quantity()`. Names are the + camelCase element IDs used in JSON, for example `Nameplate.create(manufacturerName="...")`. + An unknown name raises `KeyError`. +- A path with `[*]` is one item of a repeated list. Build items with the composite class shown in + the table and pass their elements as a tuple, for example + `BillOfMaterials.create(materialSet=(Material.create(name="Oak").element,))`. +- Dotted paths without `[*]` are nested collections; build them with the composite class shown + (for example `Contact`, `Address`). +- **Unit** names the unit dimension. Set such values with + `with_quantity(name, Dpp4FunQuantity(value=..., unit=...))`; the matching `...Unit` field is + filled for you. +- **Occurs** is how often the field may appear (`0..1` optional, `0..*` any number). + +All fields are optional at construction. Run `validate_dpp4fun()` to check the profile rules. +""" + + +def _composites() -> dict[str, str]: + """Map profile paths to the composite class names that build them.""" + sys.path.insert(0, str(ROOT / "src")) + from dpp_sdk.dpp4fun import composites + + names: dict[str, list[str]] = {} + for name in sorted(dir(composites)): + value = getattr(composites, name) + path = getattr(value, "profile_path", None) + if isinstance(value, type) and isinstance(path, str) and not name.startswith("_"): + names.setdefault(path, []).append(f"`{name}`") + return {path: " / ".join(found) for path, found in names.items()} + + +def _occurs(definition: dict[str, object]) -> str: + occurrence = definition.get("bindingOccurrence") or {} + assert isinstance(occurrence, dict) + low = occurrence.get("min", 0) + high = occurrence.get("max") + return f"{low}..{'*' if high is None else high}" + + +def _unit(definition: dict[str, object]) -> str: + dimension = definition.get("unitDimension") + if not dimension or str(dimension).endswith("-unit"): + return "" + return str(dimension) + + +def _section_title(root: str, applies_to: str) -> tuple[str, str]: + if applies_to.startswith("characteristics-"): + variant = applies_to.removeprefix("characteristics-") + cls = variant[0].upper() + variant[1:] + "Characteristics" + return f"{cls} (`characteristics`)", cls + cls = root[0].upper() + root[1:] + return f"{cls} (`{root}`)", cls + + +def render() -> str: + profile = json.loads(PROFILE.read_text(encoding="utf-8")) + composites = _composites() + sections: dict[tuple[str, str], list[dict[str, object]]] = {} + for definition in profile["definitions"]: + path = str(definition["path"]) + root = path.split(".")[0].split("[")[0] + sections.setdefault((root, definition["appliesTo"][0]), []).append(definition) + + general = sorted(key for key in sections if key[1] == "general") + variants = sorted(key for key in sections if key[1] != "general") + lines = [HEADER] + for heading, keys in (("General aspects", general), ("Product types", variants)): + lines.append(f"## {heading}\n") + for root, applies_to in keys: + title, cls = _section_title(root, applies_to) + lines.append(f"### {title}\n") + lines.append(f"Build with `{cls}.create(...)`.\n") + lines.append("| Field | Kind | Datatype | Unit | Occurs | Build with |") + lines.append("| --- | --- | --- | --- | --- | --- |") + for definition in sections[(root, applies_to)]: + path = str(definition["path"]) + if path == root: + continue + field = path.removeprefix(root + ".") + kind = KINDS.get(str(definition["objectType"]), str(definition["objectType"])) + datatype = definition.get("valueDataType") or "" + composite = composites.get(path, "") + lines.append( + f"| `{field}` | {kind} | {datatype} | {_unit(definition)} " + f"| {_occurs(definition)} | {composite} |" + ) + lines.append("") + return "\n".join(lines).rstrip() + "\n" + + +if __name__ == "__main__": + OUTPUT.write_text(render(), encoding="utf-8", newline="\n") + print(f"wrote {OUTPUT.relative_to(ROOT)}") diff --git a/scripts/render_diagrams.ps1 b/scripts/render_diagrams.ps1 index f735f8f..a3d4c1a 100644 --- a/scripts/render_diagrams.ps1 +++ b/scripts/render_diagrams.ps1 @@ -13,8 +13,17 @@ $ErrorActionPreference = "Stop" $repositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path $architectureDirectory = Join-Path $repositoryRoot "docs\architecture" $renderer = "ghcr.io/mermaid-js/mermaid-cli/mermaid-cli:11.4.2@sha256:99c983b3ab4e14033f2880bc1b9de17e5090b4515dabd63fe9cf8c0ae6130956" -$sources = Get-ChildItem -LiteralPath $architectureDirectory -Filter *.mmd -File | Sort-Object Name -if ($sources.Count -eq 0) { throw "No canonical Mermaid sources found" } +$stems = @( + "python-sdk-overview", + "python-core-model", + "python-dpp4fun-model", + "python-client-request-flow" +) +$sources = foreach ($stem in $stems) { + $source = Join-Path $architectureDirectory "$stem.mmd" + if (!(Test-Path -LiteralPath $source)) { throw "Missing Mermaid source: $source" } + Get-Item -LiteralPath $source +} foreach ($source in $sources) { $output = Join-Path $architectureDirectory ($source.BaseName + ".svg") diff --git a/src/dpp_sdk/__init__.py b/src/dpp_sdk/__init__.py index 1010040..dcce9e4 100644 --- a/src/dpp_sdk/__init__.py +++ b/src/dpp_sdk/__init__.py @@ -1,76 +1,114 @@ -"""Python SDK for Digital Product Passports (DPP). - -Idiomatic Pydantic v2 implementation of the DPP SDK contracts. - -Sub-packages: - dpp_sdk.core -- reusable core DPP model, validation, identifiers - dpp_sdk.dpp4fun -- furniture-specific DPP aggregate, validation, JSON transport - dpp_sdk.clients -- HTTP clients for the DPP repository and registry APIs -""" +"""Python SDK for canonical Digital Product Passports and the DPP4Fun profile.""" from .core import ( - Address, - Contact, - Documentation, - Dpp, - DppCore, + CompressedElementExpansionResult, + CompressedExpansionResult, + DataElement, + DataElementCollection, + DataElementKind, + DataFormatValidator, + DigitalProductPassport, DppError, + DppHeader, + DppJsonCodec, + DppJsonException, DppMappingError, DppValidationError, - Email, - Nameplate, - Organization, - OrganizationRole, - PassportMetadata, - Telephone, - validate_dpp_core, + DppValidator, + EconomicOperatorIdentifierMethod, + ExpansionIssue, + FacilityIdentifierMethod, + Granularity, + IdentifierValidationContext, + IdentifierValidator, + MultiLanguageDataElement, + MultiLanguageValue, + MultiValuedDataElement, + ProductIdentifierMethod, + RelatedResource, + SemanticDefinition, + SemanticPath, + SemanticResolver, + SingleValuedDataElement, + ValidationIssue, + ValidationResult, + ValidationSeverity, + ValidationStatus, ) from .dpp4fun import ( + DPP4FUN_PROFILE, BillOfMaterials, - Characteristics, - Component, - Dimensions, + ChairCharacteristics, + CribCharacteristics, Dpp4Fun, Dpp4FunJsonCodec, - Material, - Part, - ProductClassification, + Dpp4FunProductVariant, + Dpp4FunSemanticResolver, + Dpp4FunValidationService, + Dpp4FunValidator, + FabricCharacteristics, + LockerCharacteristics, + ParticleBoardCharacteristics, + SofaCharacteristics, from_json, from_json_and_validate, to_json, validate_dpp4fun, ) -__version__ = "0.4.0" +__version__ = "0.5.0" __all__ = [ - "Address", "BillOfMaterials", - "Characteristics", - "Component", - "Contact", - "Dimensions", - "Documentation", - "Dpp", + "ChairCharacteristics", + "CompressedElementExpansionResult", + "CompressedExpansionResult", + "CribCharacteristics", + "DPP4FUN_PROFILE", + "DataElement", + "DataElementCollection", + "DataElementKind", + "DataFormatValidator", + "DigitalProductPassport", "Dpp4Fun", "Dpp4FunJsonCodec", - "DppCore", + "Dpp4FunProductVariant", + "Dpp4FunSemanticResolver", + "Dpp4FunValidationService", + "Dpp4FunValidator", "DppError", + "DppHeader", + "DppJsonCodec", + "DppJsonException", "DppMappingError", "DppValidationError", - "Email", - "Material", - "Nameplate", - "Organization", - "OrganizationRole", - "PassportMetadata", - "Part", - "ProductClassification", - "Telephone", + "DppValidator", + "EconomicOperatorIdentifierMethod", + "ExpansionIssue", + "FabricCharacteristics", + "FacilityIdentifierMethod", + "Granularity", + "IdentifierValidationContext", + "IdentifierValidator", + "LockerCharacteristics", + "MultiLanguageDataElement", + "MultiLanguageValue", + "MultiValuedDataElement", + "ParticleBoardCharacteristics", + "ProductIdentifierMethod", + "RelatedResource", + "SemanticDefinition", + "SemanticPath", + "SemanticResolver", + "SingleValuedDataElement", + "SofaCharacteristics", + "ValidationIssue", + "ValidationResult", + "ValidationSeverity", + "ValidationStatus", "__version__", "from_json", "from_json_and_validate", "to_json", "validate_dpp4fun", - "validate_dpp_core", ] diff --git a/src/dpp_sdk/clients/README.md b/src/dpp_sdk/clients/README.md index 3dbbd4d..4c2e307 100644 --- a/src/dpp_sdk/clients/README.md +++ b/src/dpp_sdk/clients/README.md @@ -1,318 +1,9 @@ # DPP SDK clients module -## Purpose and scope +`DppRepoClient` and `DppRegistryClient` call an EN 18222 repository and registry over HTTP. They +send and return plain JSON and do not build or validate passport models; use the codecs in +`dpp_sdk.core` or `dpp_sdk.dpp4fun` for that. -Use dpp_sdk.clients to call public repository and registry APIs synchronously. DppRepoClient[T] uses a caller-supplied codec and validator for a complete passport of type T; DppRegistryClient registers metadata only. This is the canonical owner for client routes, payloads, response envelopes, and client errors. The [SDK usage guide](../../../docs/usage.md) owns the walkthrough and the [model guide](../../../docs/model-guide.md) owns full DPP field definitions. - -The module does not run a service or provide authentication, retries, async clients, persistence, internal registry APIs, lifecycle storage, or Docker runtime support. - -## Request flow and prerequisites - -![Python client request flow](../../../docs/architecture/python-client-request-flow.svg) - -`DppRepoClient` sends complete or partial passport data to a configured repository API using a -caller-supplied codec and validator. `DppRegistryClient` sends registration metadata to a -configured registry API. The clients perform HTTP calls but do not run either service. - -Create a repository client with a base URL, DppCodec[T], and validator. The built-in furniture pair is Dpp4FunJsonCodec() and validate_dpp4fun. A client-created HTTPX client is closed by close() or the with block; an injected client remains caller-owned. - -~~~python -from dpp_sdk import Dpp4FunJsonCodec, validate_dpp4fun -from dpp_sdk.clients import DppRepoClient - -with DppRepoClient("https://repo.example.com", Dpp4FunJsonCodec(), validate_dpp4fun) as repository: - assert repository.health_check() is True -~~~ - -All API calls use Accept: application/json; calls with a body additionally use Content-Type: application/json. Dynamic DPP IDs, product IDs, dates, and element paths are percent-encoded. The complete API response uses the following envelope, except health probes, which consume a bare service response. - -## Shared response envelope, DTOs, and errors - -| Envelope field | Type and null behavior | Client behavior | -| --- | --- | --- | -| statusCode | DppStatusCode or null | Required by normal API calls. Success, SuccessCreated, SuccessAccepted, and SuccessNoContent are successful. Missing/unrecognized is mapping failure. ClientErrorNotAuthorized and ClientErrorForbidden are accepted input aliases. | -| payload | JSON value or null | Required for value-returning operations. A missing/null required payload raises DppMappingClientError. Delete does not require it. | -| messages | list[DppApiMessage] or null | Each message may contain nullable messageType, text, code, correlationId, and timestamp. | - -DppApiResponse, DppApiMessage, CreateDppResponse, DeleteDppResponse, ReadDppIdsRequest, ReadDppIdsResponse, RegisterDppRequest, and RegisterDppResponse ignore unknown input fields. The response envelope is parsed before an operation-specific payload is read. - -| Error | When it is raised | -| --- | --- | -| DppValidationClientError | The supplied validator rejects a complete DPP before create_dpp() sends a request. | -| DppMappingClientError | Request serialization, malformed/incomplete envelope, missing required payload field, or full-payload codec mapping fails; the originating error is retained as a cause where applicable. | -| DppNetworkClientError | HTTPX transport or timeout failure on a normal API call. | -| DppHttpClientError | A normal API call returns non-2xx HTTP; it exposes status_code and response_body. | -| DppApiClientError | A 2xx response envelope carries a non-success statusCode; it exposes parsed status, messages, and raw body. | - -## Repository operation reference - -Every normal API operation below accepts any 2xx HTTP response only when its envelope has a successful statusCode. “Shared errors” means the preceding error table applies. - -### health_check - -**Purpose:** repository reachability. **Signature:** health_check() -> bool. **HTTP:** GET /health; no path/query/body and Accept only. **Success/response:** any 2xx returns True; non-2xx, timeout, and transport failure return False. The bare health body is not parsed as an API envelope. **Null/errors:** not applicable; it never raises client HTTP/network errors. **Limit:** reachability does not guarantee later API success. **Evidence:** repo.py:DppRepoClient.health_check; _http.py:probe_health; `tests/test_python_only_extensions.py:test_health_helpers_return_reachability_without_raising`. - -### create_dpp - -**Purpose:** validate and create a complete passport. **Signature:** create_dpp(dpp: T) -> CreateDppResponse. **HTTP:** POST /v1/dpps; no path/query. - -| Request | Definition | -| --- | --- | -| Body type | Non-null JSON string from the supplied DppCodec.to_json(dpp), after the supplied validator passes. | -| Fields/example | The caller codec owns the full-DPP field table; use the [model guide](../../../docs/model-guide.md). | -| Null/mapping behavior | Validator failure becomes DppValidationClientError. Codec failure or null output becomes DppMappingClientError before I/O. | - -| Success response field | Definition | -| --- | --- | -| Envelope | Successful statusCode and non-null object payload. | -| dppId | CreateDppResponse field, nullable in its DTO but required non-blank by this client method. | - -~~~json -{"statusCode":"SuccessCreated","payload":{"dppId":"DPP-1"},"messages":null} -~~~ - -**Limit:** no domain schema beyond the caller codec. **Errors:** shared errors plus the request rules above. **Evidence:** repo.py:create_dpp; _http.py:require_text_field; test_create_returns_dpp_id; test_validator_failure_precedes_network_and_preserves_sdk_error. - -### read_dpp_by_id - -**Purpose:** full typed passport by DPP ID. **Signature:** read_dpp_by_id(dpp_id: str) -> T. **HTTP:** GET /v1/dpps/{dppId}?representation=full. - -| Parameters and request | Definition | -| --- | --- | -| dppId path | Required non-blank str; percent-encoded. | -| query/body | representation=full; no body. | -| success response | Successful envelope with required non-null full JSON payload; the caller codec decodes it to T. | - -~~~json -{"statusCode":"Success","payload":{"callerCodecOwned":"full DPP JSON"},"messages":null} -~~~ - -**Null/errors:** null payload or codec failure is DppMappingClientError; shared errors apply. **Limit:** full-DPP fields are owned by the caller codec. **Evidence:** repo.py:read_dpp_by_id; test_read_by_id_decodes_via_codec. - -### read_dpp_by_product_id - -**Purpose:** full typed passport by product ID. **Signature:** read_dpp_by_product_id(product_id: str) -> T. **HTTP:** GET /v1/dppsByProductId/{productId}?representation=full. - -| Parameters and request | Definition | -| --- | --- | -| productId path | Required non-blank str; percent-encoded. | -| query/body | representation=full; no body. | -| success response | Successful envelope with required non-null full JSON payload decoded to T by the caller codec. | - -~~~json -{"statusCode":"Success","payload":{"callerCodecOwned":"full DPP JSON"},"messages":null} -~~~ - -**Null/errors:** null payload or codec failure is mapping failure; shared errors apply. **Limit:** full representation only. **Evidence:** repo.py:read_dpp_by_product_id; test_read_by_product_id_encodes_path. - -### read_compressed_dpp_by_id - -**Purpose:** raw project-defined compressed representation. **Signature:** read_compressed_dpp_by_id(dpp_id: str) -> Any. **HTTP:** GET /v1/dpps/{dppId}?representation=compressed. - -| Parameters and request | Definition | -| --- | --- | -| dppId path | Required non-blank str; percent-encoded. | -| query/body | representation=compressed; no body. | -| success response | Successful envelope with required non-null raw JSON payload, returned without codec conversion. | - -~~~json -{"statusCode":"Success","payload":{"format":"service-defined"},"messages":null} -~~~ - -**Null/errors:** null payload is mapping failure; shared errors apply. **Limit:** no decompression or typed Dpp4Fun conversion. **Evidence:** repo.py:read_compressed_dpp_by_id; test_read_compressed_dpp_returns_raw_payload_without_codec. - -### read_dpp_version_by_id_and_date - -**Purpose:** full historical version by DPP ID and instant. **Signature:** read_dpp_version_by_id_and_date(dpp_id: str, date: datetime) -> T. **HTTP:** GET /v1/dppsByIdAndDate/{dppId}?date={UTC-instant}&representation=full. - -| Parameters and request | Definition | -| --- | --- | -| dppId path | Required non-blank str; percent-encoded. | -| date query | Required timezone-aware datetime; normalized to UTC Z (seconds, milliseconds, or microseconds as required), then percent-encoded. | -| body/response | No body; successful envelope needs non-null full JSON payload decoded to T. | - -**Example route:** /v1/dpps/DPP-1?date=2026-06-29T10%3A00%3A00Z&representation=full. **Null/errors:** naive datetime or blank ID raises ValueError before I/O; null payload/codec failure is mapping failure; shared errors apply. **Limit:** use this public versioned route for new code. **Evidence:** repo.py:read_dpp_version_by_id_and_date and _instant; test_dpp_id_history_uses_versioned_path_and_ordered_query; test_history_timestamp_wire_is_canonical_utc_z. - -### read_dpp_version_by_product_id_and_date - -**Purpose:** legacy product-ID history compatibility. **Signature:** read_dpp_version_by_product_id_and_date(product_id: str, date: datetime) -> T. **HTTP:** GET /dppsByProductIdAndDate/{productId}?date={UTC-instant}. - -| Parameters and request | Definition | -| --- | --- | -| productId/date | Required non-blank encoded product ID and timezone-aware UTC-normalized instant. | -| body/response | No body; successful envelope requires non-null full JSON payload decoded to T. | - -~~~json -{"statusCode":"Success","payload":{"callerCodecOwned":"full DPP JSON"},"messages":null} -~~~ - -**Null/errors:** same pre-I/O and shared errors as DPP-ID history. **Limit:** deprecated unversioned compatibility route; new code uses read_dpp_version_by_id_and_date. **Evidence:** repo.py:read_dpp_version_by_product_id_and_date; test_product_id_history_is_retained_unversioned_compatibility. - -### read_dpp_ids_by_product_ids - -**Purpose:** look up DPP IDs for product IDs. **Signature:** read_dpp_ids_by_product_ids(product_ids: list[str], limit: int | None = None, cursor: str | None = None) -> ReadDppIdsResponse. **HTTP:** POST /v1/dppsByProductIds; no path/query. - -**Request body:** one ReadDppIdsRequest JSON object. - -| Request field | Type and null behavior | -| --- | --- | -| productIdentifiers | list[str] or null; emitted even when None. | -| limit | int or null; emitted even when None. | -| cursor | str or null; emitted even when None. | - -~~~json -{"productIdentifiers":["GTIN-0001"],"limit":25,"cursor":null} -~~~ - -| Response field | Type and method requirement | -| --- | --- | -| dppIdentifiers | list[str] or null in DTO; required non-null by this method. | -| nextCursor | str or null; may be null. | - -~~~json -{"statusCode":"Success","payload":{"dppIdentifiers":["DPP-1"],"nextCursor":null},"messages":null} -~~~ - -**Errors/limit:** missing payload/dppIdentifiers is mapping failure; shared errors apply. The SDK forwards pagination values but does not validate product-ID syntax or provide a pagination iterator. **Evidence:** repo.py:read_dpp_ids_by_product_ids; payloads.py:ReadDppIdsRequest/ReadDppIdsResponse; test_read_dpp_ids; test_repository_dtos_preserve_nulls_defaults_and_unknown_field_policy. - -### update_dpp_by_id - -**Purpose:** send a partial JSON value and return a typed full passport. **Signature:** update_dpp_by_id(dpp_id: str, partial_dpp: Any) -> T. **HTTP:** PATCH /v1/dpps/{dppId}. - -| Parameters and request | Definition | -| --- | --- | -| dppId path | Required non-blank str; percent-encoded. | -| body | Direct json.dumps(partial_dpp, allow_nan=False); None emits null. | -| success response | Successful envelope with non-null full JSON payload decoded to T. | - -~~~json -{"characteristics":{"productName":"ErgoChair Pro"}} -~~~ - -**Example response:** {"statusCode":"Success","payload":{"callerCodecOwned":"full DPP JSON"},"messages":null}. **Null/errors:** NaN, infinity, sets, and non-JSON values raise DppMappingClientError before I/O; null response/codec failure is mapping failure; shared errors apply. **Limit:** direct JSON forwarding, not complete RFC 7396 merge-patch behavior. **Evidence:** repo.py:update_dpp_by_id and _serialize_partial_update; test_partial_updates_reject_unmappable_json_before_transport; test_partial_patch_decodes_response_without_whole_model_validation. - -### read_data_element - -**Purpose:** retrieve one raw selected JSON value. **Signature:** read_data_element(dpp_id: str, element_path: str) -> Any. **HTTP:** GET /v1/dpps/{dppId}/elements/{elementPath}. - -| Parameters and request | Definition | -| --- | --- | -| dppId/elementPath | Required non-blank str values; each percent-encoded as one path segment. | -| body/response | No body; successful envelope requires non-null raw JSON payload returned unchanged. | - -~~~json -{"statusCode":"Success","payload":"ErgoChair","messages":null} -~~~ - -**Null/errors:** blank inputs fail before I/O; null payload is mapping failure; shared errors apply. **Limit:** the client sends the path but does not evaluate JSONPath or implement complete RFC 9535 behavior. **Evidence:** repo.py:read_data_element; test_read_data_element_returns_the_raw_selected_json_value. - -### update_data_element - -**Purpose:** replace one selected raw JSON value. **Signature:** update_data_element(dpp_id: str, element_path: str, payload: Any) -> Any. **HTTP:** PATCH /v1/dpps/{dppId}/elements/{elementPath}. - -| Parameters and request | Definition | -| --- | --- | -| paths | Required non-blank dppId and elementPath; both percent-encoded. | -| body | Direct strict JSON value; None emits null; not an UpdateDataElementRequest wrapper. | -| response | Successful envelope with required non-null raw JSON value returned unchanged. | - -~~~json -"Updated element value" -~~~ - -~~~json -{"statusCode":"Success","payload":"Updated element value","messages":null} -~~~ - -**Null/errors:** non-representable/non-finite request data is mapping failure before I/O; null payload is mapping failure; shared errors apply. UpdateDataElementRequest(payload=...) remains importable only for compatibility. **Limit:** no local JSONPath evaluation/complete RFC 9535 support. **Evidence:** repo.py:update_data_element; test_update_data_element_sends_direct_payload; test_compatibility_element_dto_cannot_change_canonical_direct_body. - -### delete_dpp_by_id - -**Purpose:** delete one DPP. **Signature:** delete_dpp_by_id(dpp_id: str) -> DeleteDppResponse. **HTTP:** DELETE /v1/dpps/{dppId}. - -| Parameters and response | Definition | -| --- | --- | -| dppId/body | Required non-blank encoded path; no request body. | -| statusCode | DppStatusCode or null, copied to DeleteDppResponse. | -| messages | list[DppApiMessage] or null, copied to DeleteDppResponse. | -| payload | Not required; a successful SuccessNoContent response may omit it or use null. | - -~~~json -{"statusCode":"SuccessNoContent","payload":null,"messages":null} -~~~ - -**Errors/limit:** shared HTTP/API/network/mapping errors apply before the DTO is returned. The result contains envelope status/messages only. **Evidence:** repo.py:delete_dpp_by_id; payloads.py:DeleteDppResponse; test_delete_returns_status. - -## Registry operation reference - -### health_check - -**Purpose:** registry reachability. **Signature:** health_check() -> bool. **HTTP:** GET /health; no parameters/body and Accept only. **Success/response:** bare health body is ignored; 2xx returns True, non-2xx/timeout/transport returns False. **Null/errors:** not applicable. **Limit:** no registration guarantee. **Evidence:** registry.py:DppRegistryClient.health_check; _http.py:probe_health; `tests/test_python_only_extensions.py:test_health_helpers_return_reachability_without_raising`. - -### post_new_dpp_to_registry - -**Purpose:** register passport metadata. **Signature:** post_new_dpp_to_registry(request: RegisterDppRequest) -> RegisterDppResponse. **HTTP:** POST /v1/registerDPP; no path/query. - -**Request body:** one RegisterDppRequest JSON object. - -| Request field | Type, alias, and null behavior | -| --- | --- | -| uniqueProductIdentifier | str or null; accepts input alias productIdentifier; emitted under the current name. | -| digitalProductPassportId | str or null; accepts dppIdentifier; emitted under the current name. | -| uniqueEconomicOperatorIdentifier | str or null; accepts operatorIdentifier; emitted under the current name. | -| dppApiEndpoint | str or null; accepts repoUrl; emitted under the current name. | - -~~~json -{"uniqueProductIdentifier":"GTIN-0001","digitalProductPassportId":"DPP-1","uniqueEconomicOperatorIdentifier":"operator-123","dppApiEndpoint":"https://repo.example.com"} -~~~ - -The request object itself cannot be None; null/unserializable request maps to DppMappingClientError before I/O. DTO fields may be null and are emitted as null. - -| Response field | Type and method requirement | -| --- | --- | -| registrationId | str or null in RegisterDppResponse, but required non-blank by this client method; registryIdentifier is its input alias. | - -~~~json -{"statusCode":"SuccessCreated","payload":{"registrationId":"REG-1"},"messages":null} -~~~ - -**Errors/limit:** malformed payload or blank/missing registrationId is mapping failure; shared errors apply. There is no registry read-back, delete, cleanup, backup-operator, or internal-route operation. **Evidence:** registry.py:post_new_dpp_to_registry; payloads.py:RegisterDppRequest/RegisterDppResponse; test_registry_register_uses_canonical_wire_contract; test_registry_payload_canonical_keys_and_legacy_input_aliases. - -## Examples and limitations - -Use DTOs without the supplied clients only when your application owns HTTP transport: - -~~~python -from dpp_sdk.clients import ReadDppIdsRequest, ReadDppIdsResponse - -outgoing_json = ReadDppIdsRequest(productIdentifiers=["GTIN-0001"], limit=25).model_dump_json() -incoming_json = '{"dppIdentifiers":["DPP-1"],"nextCursor":null}' -assert ReadDppIdsResponse.model_validate_json(incoming_json).dppIdentifiers == ["DPP-1"] -~~~ - -Service-specific status behavior belongs to the optional [Mock-services demo](../../../examples/mock-services-demo/README.md), not to this generic client contract. Compression, partial update, and element-path limitations are stated with their operations above. - -## Focused client checks - -**Purpose:** run deterministic controlled-HTTP client checks. **Run from:** repository root. -**Prerequisites:** the checkout development environment. [RELEASING.md](../../../RELEASING.md) owns -installation, full validation, builds, archive inspection, and cleanup. - -### PowerShell - -```powershell -.\.venv\Scripts\python.exe -m pytest tests/test_clients.py tests/test_end_to_end.py -``` - -### Linux/macOS - -```bash -.venv/bin/python -m pytest tests/test_clients.py tests/test_end_to_end.py -``` - -**Expected result:** controlled client and end-to-end SDK tests pass without a live service. -**Cleanup:** none. - -## Related documents and next steps - -Use [SDK usage](../../../docs/usage.md) for a first integration, the [model guide](../../../docs/model-guide.md) for complete DPP fields, the [validation guide](../../../docs/validation-guide.md) for codec/validation stages, and [RELEASING.md](../../../RELEASING.md) for development and release commands. +- [HTTP clients guide](../../../docs/clients-guide.md): every method, error, and option. +- [Decode a response into a typed passport](../../../docs/clients-guide.md#decode-a-response-into-a-typed-passport). +- [Contributing](../../../CONTRIBUTING.md): tests and checks for this module. diff --git a/src/dpp_sdk/clients/__init__.py b/src/dpp_sdk/clients/__init__.py index acad5c1..c677470 100644 --- a/src/dpp_sdk/clients/__init__.py +++ b/src/dpp_sdk/clients/__init__.py @@ -1,67 +1,45 @@ -"""HTTP clients for the DPP repository and registry APIs (prEN-18222-aligned). +"""Generic JSON clients for DPP repository and registry APIs.""" -Provides repository and registry HTTP client contracts. -""" - -from ._http import DppCodec, DppValidator -from .endpoints import ( - DEFAULT_REGISTRY_BASE_URL, - DEFAULT_REGISTRY_PORT, - DEFAULT_REPO_BASE_URL, - DEFAULT_REPO_PORT, - local_registry_base_url, - local_repo_base_url, -) from .errors import ( DppApiClientError, DppClientError, DppHttpClientError, DppMappingClientError, DppNetworkClientError, - DppValidationClientError, ) from .payloads import ( CreateDppResponse, DeleteDppResponse, DppApiMessage, DppApiResponse, + DppRepresentation, DppStatusCode, MessageType, ReadDppIdsRequest, ReadDppIdsResponse, RegisterDppRequest, RegisterDppResponse, - UpdateDataElementRequest, ) from .registry import DppRegistryClient from .repo import DppRepoClient __all__ = [ - "DEFAULT_REGISTRY_BASE_URL", - "DEFAULT_REGISTRY_PORT", - "DEFAULT_REPO_BASE_URL", - "DEFAULT_REPO_PORT", "CreateDppResponse", "DeleteDppResponse", "DppApiClientError", "DppApiMessage", "DppApiResponse", "DppClientError", - "DppCodec", "DppHttpClientError", "DppMappingClientError", "DppNetworkClientError", "DppRegistryClient", "DppRepoClient", + "DppRepresentation", "DppStatusCode", - "DppValidationClientError", - "DppValidator", "MessageType", "ReadDppIdsRequest", "ReadDppIdsResponse", "RegisterDppRequest", "RegisterDppResponse", - "UpdateDataElementRequest", - "local_registry_base_url", - "local_repo_base_url", ] diff --git a/src/dpp_sdk/clients/_http.py b/src/dpp_sdk/clients/_http.py index cf08ffe..ef8ae75 100644 --- a/src/dpp_sdk/clients/_http.py +++ b/src/dpp_sdk/clients/_http.py @@ -1,13 +1,11 @@ -"""Shared HTTP transport and JSON helpers (port of ``HttpSupport``). - -Provides base-URL handling, URL encoding, the request/parse/validate pipeline for -the API wrapper envelope, and the ``DppCodec`` / ``DppValidator`` typing contracts. -""" +"""Shared synchronous HTTP and API-envelope handling.""" from __future__ import annotations -from typing import Any, Protocol, TypeVar -from urllib.parse import quote +import json +import re +from typing import Any, NoReturn +from urllib.parse import quote, urlsplit import httpx from pydantic import ValidationError @@ -18,29 +16,12 @@ DppMappingClientError, DppNetworkClientError, ) -from .payloads import DppApiResponse - -T = TypeVar("T") -T_contra = TypeVar("T_contra", contravariant=True) +from .payloads import DppApiResponse, DppStatusCode DEFAULT_CONNECT_TIMEOUT = 5.0 DEFAULT_REQUEST_TIMEOUT = 15.0 -class DppCodec(Protocol[T]): - """Converts a caller-owned DPP type to and from full JSON payloads.""" - - def to_json(self, dpp: T) -> str: ... - - def from_json(self, raw: str) -> T: ... - - -class DppValidator(Protocol[T_contra]): - """Validates a caller-owned DPP type before a create request is sent.""" - - def __call__(self, dpp: T_contra) -> None: ... - - def build_client() -> httpx.Client: return httpx.Client( timeout=httpx.Timeout(DEFAULT_REQUEST_TIMEOUT, connect=DEFAULT_CONNECT_TIMEOUT) @@ -51,34 +32,45 @@ def normalize_base_url(base_url: str) -> str: trimmed = base_url.strip() if not trimmed: raise ValueError("baseUrl must not be blank") + try: + parsed = urlsplit(trimmed) + host = parsed.hostname + port = parsed.port + except ValueError as exc: + raise ValueError("baseUrl must be a valid URI") from exc + if ( + re.search(r"%(?![0-9A-Fa-f]{2})", trimmed) + or any(char in trimmed for char in '<>"{}|\\^`') + or any(ord(char) <= 0x20 for char in trimmed) + ): + raise ValueError("baseUrl must be a valid URI") + if ( + parsed.scheme.lower() not in {"http", "https"} + or host is None + or (port is not None and not 0 <= port <= 65535) + or "?" in trimmed + or "#" in trimmed + ): + raise ValueError( + "baseUrl must be an absolute HTTP(S) URL with a host and no query/fragment" + ) return trimmed.rstrip("/") def resolve(base_url: str, path_and_query: str) -> str: - normalized = path_and_query if path_and_query else "/" - if not normalized.startswith("/"): - normalized = "/" + normalized - return base_url + normalized + path = path_and_query or "/" + return base_url + (path if path.startswith("/") else f"/{path}") def encode_segment(value: str) -> str: return quote(value, safe="*").replace("~", "%7E") -def probe_health(client: httpx.Client, base_url: str) -> bool: - """Probe ``GET {base_url}/health`` and report reachability. - - Mirrors the Java ``DemoServicePreflight.probe``: returns ``True`` only on a 2xx - response and ``False`` on any non-2xx status or transport/timeout error. The mock - ``/health`` endpoint returns a bare ``HealthPayload`` (not the wrapped DPP API - envelope), so this deliberately bypasses :func:`request_api`. - """ - url = resolve(normalize_base_url(base_url), "/health") +def serialize_json(value: Any) -> str: try: - response = client.request("GET", url, headers={"Accept": "application/json"}) - except (httpx.TimeoutException, httpx.TransportError): - return False - return 200 <= response.status_code < 300 + return json.dumps(value, ensure_ascii=False, separators=(",", ":"), allow_nan=False) + except (TypeError, ValueError) as exc: + raise DppMappingClientError("Client JSON serialization failed before request") from exc def request_api( @@ -86,8 +78,9 @@ def request_api( method: str, url: str, body: str | None, + *, + allow_no_content: bool = False, ) -> DppApiResponse: - """Run the full request pipeline and return a validated success envelope.""" headers = {"Accept": "application/json"} if body is not None: headers["Content-Type"] = "application/json" @@ -98,68 +91,77 @@ def request_api( except httpx.TransportError as exc: raise DppNetworkClientError("DPP HTTP request could not complete") from exc - _require_http_success(response) - api_response = _parse_api_response(response.text) - _require_api_success(api_response, response.text) - return api_response - - -def _require_http_success(response: httpx.Response) -> None: if not 200 <= response.status_code < 300: - raise DppHttpClientError( - f"DPP endpoint returned non-success HTTP status {response.status_code}", - response.status_code, - response.text, - ) + _raise_http(response.status_code, response.text) + if response.status_code == 204 and allow_no_content: + return DppApiResponse(status_code=DppStatusCode.SuccessNoContent) + api_response = parse_response(response.text) + _require_success(api_response, response.text) + return api_response -def _parse_api_response(raw_body: str) -> DppApiResponse: +def parse_response(raw_body: str) -> DppApiResponse: try: return DppApiResponse.model_validate_json(raw_body) - except ValidationError as exc: + except (ValidationError, ValueError) as exc: raise DppMappingClientError("DPP API response could not be mapped") from exc -def _require_api_success(response: DppApiResponse, raw_body: str) -> None: - status_code = response.statusCode - if status_code is None: - cause = ValueError("Missing required response field: statusCode") - raise DppMappingClientError( - "DPP API response could not be mapped: missing statusCode" - ) from cause - if not status_code.is_success: +def _raise_http(status: int, raw_body: str) -> None: + try: + api_response = parse_response(raw_body) + if api_response.status_code is None or api_response.status_code.is_success: + raise ValueError("Error response has no error statusCode") + except DppMappingClientError as exc: + raise DppHttpClientError( + f"DPP endpoint returned non-success HTTP status {status}", status, raw_body + ) from exc + except ValueError as exc: + raise DppHttpClientError( + f"DPP endpoint returned non-success HTTP status {status}", status, raw_body + ) from exc + raise DppApiClientError( + f"DPP endpoint returned HTTP {status}", + status, + api_response.status_code, + api_response.message, + raw_body, + ) + + +def _require_success(response: DppApiResponse, raw_body: str) -> None: + if response.status_code is None: + cause = ValueError("Missing statusCode") + raise DppMappingClientError("DPP API response could not be mapped") from cause + if not response.status_code.is_success: raise DppApiClientError( - f"DPP API returned error status {status_code.value}", - status_code, - response.messages, + f"DPP API returned error status {response.status_code.value}", + -1, + response.status_code, + response.message, raw_body, ) def require_payload(response: DppApiResponse) -> Any: - payload = response.payload - if payload is None: - raise _missing_field("payload") - return payload - - -def require_text_field(payload: Any, field_name: str, field_path: str) -> str: - if not isinstance(payload, dict): - raise _missing_field(field_path) - value = payload.get(field_name) - if not isinstance(value, str) or not value.strip(): - raise _missing_field(field_path) - return value + if response.payload is None: + _missing_field("payload") + return response.payload def require_field(payload: Any, field_name: str, field_path: str) -> Any: if not isinstance(payload, dict) or payload.get(field_name) is None: - raise _missing_field(field_path) + _missing_field(field_path) return payload[field_name] -def _missing_field(field_path: str) -> DppMappingClientError: +def require_text_field(payload: Any, field_name: str, field_path: str) -> str: + value = require_field(payload, field_name, field_path) + if not isinstance(value, str) or not value.strip(): + _missing_field(field_path) + return value + + +def _missing_field(field_path: str) -> NoReturn: message = f"Missing required response field: {field_path}" - error = DppMappingClientError(message) - error.__cause__ = ValueError(message) - return error + raise DppMappingClientError(message) from ValueError(message) diff --git a/src/dpp_sdk/clients/endpoints.py b/src/dpp_sdk/clients/endpoints.py deleted file mode 100644 index 8363237..0000000 --- a/src/dpp_sdk/clients/endpoints.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Default endpoint configuration for the DPP mock services. - -The Java reference SDK keeps the client constructors URL-required and resolves the -mock-service defaults in the demo runner (``HttpServiceDemoRunner`` / -``DemoServicePreflight``). We mirror that split here: the clients still require a -``base_url``, while these helpers provide the local mock defaults used by the -``for_local_mock`` factories. - -Defaults match ``dpp-sdk-demo``: the mock repo on ``http://localhost:8080`` and the -mock registry on ``http://localhost:8081``. Both are env-overridable — a full-URL -override (``DPP_REPO_BASE_URL`` / ``DPP_REGISTRY_BASE_URL``) takes precedence, otherwise -the port is taken from ``DPP_REPO_PORT`` / ``DPP_REGISTRY_PORT`` (matching the Java -``defaultRepoLocalUrl()`` / ``defaultRegistryLocalUrl()``). -""" - -from __future__ import annotations - -import os - -DEFAULT_REPO_PORT = 8080 -DEFAULT_REGISTRY_PORT = 8081 - -DEFAULT_REPO_BASE_URL = f"http://localhost:{DEFAULT_REPO_PORT}" -DEFAULT_REGISTRY_BASE_URL = f"http://localhost:{DEFAULT_REGISTRY_PORT}" - - -def _local_base_url(url_env: str, port_env: str, default_port: int) -> str: - override = os.environ.get(url_env) - if override and override.strip(): - return override.strip() - port = os.environ.get(port_env, str(default_port)).strip() or str(default_port) - return f"http://localhost:{port}" - - -def local_repo_base_url() -> str: - """Resolve the local mock-repo base URL, honoring env overrides at call time.""" - return _local_base_url("DPP_REPO_BASE_URL", "DPP_REPO_PORT", DEFAULT_REPO_PORT) - - -def local_registry_base_url() -> str: - """Resolve the local mock-registry base URL, honoring env overrides at call time.""" - return _local_base_url("DPP_REGISTRY_BASE_URL", "DPP_REGISTRY_PORT", DEFAULT_REGISTRY_PORT) diff --git a/src/dpp_sdk/clients/errors.py b/src/dpp_sdk/clients/errors.py index a99a6b7..cf881a7 100644 --- a/src/dpp_sdk/clients/errors.py +++ b/src/dpp_sdk/clients/errors.py @@ -12,10 +12,6 @@ class DppClientError(Exception): """Base class for all DPP HTTP client errors.""" -class DppValidationClientError(DppClientError): - """Raised when a DPP fails validation before a request is sent.""" - - class DppMappingClientError(DppClientError): """Raised when request/response JSON cannot be serialized or parsed.""" @@ -25,7 +21,7 @@ class DppNetworkClientError(DppClientError): class DppHttpClientError(DppClientError): - """Raised when an endpoint returns a non-2xx HTTP status.""" + """Raised when an endpoint returns an unstructured non-2xx HTTP status.""" def __init__(self, message: str, status_code: int, response_body: str) -> None: super().__init__(message) @@ -34,16 +30,18 @@ def __init__(self, message: str, status_code: int, response_body: str) -> None: class DppApiClientError(DppClientError): - """Raised when a 2xx response carries an error status in the API wrapper.""" + """Raised when an API envelope reports failure, including structured HTTP errors.""" def __init__( self, - message: str, - status_code: DppStatusCode | None, - messages: list[DppApiMessage] | None, + error_message: str, + http_status: int, + status_code: DppStatusCode, + message: list[DppApiMessage] | None, raw_response_body: str, ) -> None: - super().__init__(message) + super().__init__(error_message) + self.http_status = http_status self.status_code = status_code - self.messages = messages + self.message = message self.raw_response_body = raw_response_body diff --git a/src/dpp_sdk/clients/payloads.py b/src/dpp_sdk/clients/payloads.py index c41b9a1..716f545 100644 --- a/src/dpp_sdk/clients/payloads.py +++ b/src/dpp_sdk/clients/payloads.py @@ -1,23 +1,14 @@ -"""API wrapper envelope and request/response DTOs for the DPP HTTP clients. - -Ports ``dpp.repo.payloads.*`` and ``dpp.registry.payloads.*`` (which are -identical for the shared envelope) into a single module. These models use -``extra='ignore'`` to mirror the clients' Jackson config, which disables -``FAIL_ON_UNKNOWN_PROPERTIES``. -""" +"""Neutral request and response JSON contracts for repository and registry APIs.""" from __future__ import annotations -from datetime import datetime from enum import StrEnum from typing import Any -from pydantic import AliasChoices, BaseModel, ConfigDict, Field +from pydantic import AwareDatetime, BaseModel, ConfigDict, Field class DppStatusCode(StrEnum): - """Application-level status in the API wrapper ``statusCode`` field.""" - Success = "Success" SuccessCreated = "SuccessCreated" SuccessAccepted = "SuccessAccepted" @@ -28,123 +19,96 @@ class DppStatusCode(StrEnum): ClientMethodNotAllowed = "ClientMethodNotAllowed" ClientErrorResourceNotFound = "ClientErrorResourceNotFound" ClientResourceConflict = "ClientResourceConflict" + ServerNotImplemented = "ServerNotImplemented" ServerInternalError = "ServerInternalError" ServerErrorBadGateway = "ServerErrorBadGateway" - ServerNotImplemented = "ServerNotImplemented" - - @classmethod - def _missing_(cls, value: object) -> DppStatusCode | None: - # Mirror the Java @JsonCreator aliases. - aliases = { - "ClientErrorNotAuthorized": cls.ClientNotAuthorized, - "ClientErrorForbidden": cls.ClientForbidden, - } - if isinstance(value, str): - return aliases.get(value) - return None @property def is_success(self) -> bool: return self in { - DppStatusCode.Success, - DppStatusCode.SuccessCreated, - DppStatusCode.SuccessAccepted, - DppStatusCode.SuccessNoContent, + self.Success, + self.SuccessCreated, + self.SuccessAccepted, + self.SuccessNoContent, } class MessageType(StrEnum): - """Severity/category for API wrapper messages.""" - Info = "Info" Warning = "Warning" Error = "Error" Exception = "Exception" -class _ClientBase(BaseModel): +class DppRepresentation(StrEnum): + COMPRESSED = "compressed" + FULL = "full" + + @classmethod + def from_query(cls, value: str | None) -> DppRepresentation: + if value is None or value.lower() == "compressed": + return cls.COMPRESSED + if value.lower() == "full": + return cls.FULL + raise ValueError("representation must be full or compressed") + + @property + def query_suffix(self) -> str: + return "?representation=full" if self is self.FULL else "" + + +class _Payload(BaseModel): model_config = ConfigDict(populate_by_name=True, extra="ignore") -class DppApiMessage(_ClientBase): - messageType: MessageType | None = None - text: str | None = None +class DppApiMessage(_Payload): + message_type: MessageType = Field(alias="messageType") + text: str code: str | None = None - correlationId: str | None = None - timestamp: datetime | None = None + correlation_id: str | None = Field(default=None, alias="correlationId") + timestamp: AwareDatetime | None = None -class DppApiResponse(_ClientBase): - """Standard response envelope; ``payload`` is left as raw JSON (``Any``).""" - - statusCode: DppStatusCode | None = None +class DppApiResponse(_Payload): + status_code: DppStatusCode | None = Field(default=None, alias="statusCode") payload: Any = None - messages: list[DppApiMessage] | None = None + message: list[DppApiMessage] | None = None -# --- repository DTOs ------------------------------------------------------------ -class CreateDppResponse(_ClientBase): - dppId: str | None = None +class CreateDppResponse(_Payload): + status_code: DppStatusCode | None = Field(default=None, alias="statusCode") + message: list[DppApiMessage] | None = None + digital_product_passport_id: str | None = Field(default=None, alias="digitalProductPassportId") -class DeleteDppResponse(_ClientBase): - statusCode: DppStatusCode | None = None - messages: list[DppApiMessage] | None = None +class DeleteDppResponse(_Payload): + status_code: DppStatusCode | None = Field(default=None, alias="statusCode") + message: list[DppApiMessage] | None = None -class ReadDppIdsRequest(_ClientBase): - productIdentifiers: list[str] | None = None +class ReadDppIdsRequest(_Payload): + product_identifiers: list[str] | None = Field(default=None, alias="productIdentifiers") limit: int | None = None cursor: str | None = None -class ReadDppIdsResponse(_ClientBase): - dppIdentifiers: list[str] | None = None - nextCursor: str | None = None - - -class UpdateDataElementRequest(_ClientBase): - payload: Any = None +class ReadDppIdsResponse(_Payload): + status_code: DppStatusCode | None = Field(default=None, alias="statusCode") + message: list[DppApiMessage] | None = None + dpp_identifiers: list[str] = Field(alias="dppIdentifiers") + next_cursor: str | None = Field(default=None, alias="nextCursor") -# --- registry DTOs -------------------------------------------------------------- -class RegisterDppRequest(_ClientBase): - uniqueProductIdentifier: str | None = Field( - validation_alias=AliasChoices("uniqueProductIdentifier", "productIdentifier"), default=None - ) - digitalProductPassportId: str | None = Field( - validation_alias=AliasChoices("digitalProductPassportId", "dppIdentifier"), default=None - ) - uniqueEconomicOperatorIdentifier: str | None = Field( - validation_alias=AliasChoices("uniqueEconomicOperatorIdentifier", "operatorIdentifier"), - default=None, +class RegisterDppRequest(_Payload): + unique_product_identifier: str | None = Field(default=None, alias="uniqueProductIdentifier") + digital_product_passport_id: str | None = Field(default=None, alias="digitalProductPassportId") + unique_economic_operator_identifier: str | None = Field( + default=None, alias="uniqueEconomicOperatorIdentifier" ) - dppApiEndpoint: str | None = Field( - validation_alias=AliasChoices("dppApiEndpoint", "repoUrl"), default=None - ) - - @property - def productIdentifier(self) -> str | None: # noqa: N802 - return self.uniqueProductIdentifier - - @property - def dppIdentifier(self) -> str | None: # noqa: N802 - return self.digitalProductPassportId - - @property - def operatorIdentifier(self) -> str | None: # noqa: N802 - return self.uniqueEconomicOperatorIdentifier - - @property - def repoUrl(self) -> str | None: # noqa: N802 - return self.dppApiEndpoint - + dpp_api_endpoint: str | None = Field(default=None, alias="dppApiEndpoint") -class RegisterDppResponse(_ClientBase): - registrationId: str | None = Field( - validation_alias=AliasChoices("registrationId", "registryIdentifier"), default=None - ) - @property - def registryIdentifier(self) -> str | None: # noqa: N802 - return self.registrationId +class RegisterDppResponse(_Payload): + status_code: DppStatusCode | None = Field(default=None, alias="statusCode") + message: list[DppApiMessage] | None = None + registration_id: str | None = Field(default=None, alias="registrationId") diff --git a/src/dpp_sdk/clients/registry.py b/src/dpp_sdk/clients/registry.py index 16838a0..8b7929b 100644 --- a/src/dpp_sdk/clients/registry.py +++ b/src/dpp_sdk/clients/registry.py @@ -1,47 +1,28 @@ -"""HTTP client for the DPP registry API (port of ``HttpDppRegistryClient``).""" +"""Generic JSON client for the final registry API.""" from __future__ import annotations from types import TracebackType -from typing import Self +from typing import Any, Self import httpx from pydantic import ValidationError -from . import _http, endpoints +from . import _http from .errors import DppMappingClientError from .payloads import RegisterDppRequest, RegisterDppResponse -_REGISTER_PATH = "/v1/registerDPP" - class DppRegistryClient: + """Synchronous JSON in/JSON out client for registry registration.""" + def __init__(self, base_url: str, *, client: httpx.Client | None = None) -> None: self._base_url = _http.normalize_base_url(base_url) self._owns_client = client is None self._client = client if client is not None else _http.build_client() self._closed = False - @classmethod - def for_local_mock( - cls, - *, - base_url: str | None = None, - client: httpx.Client | None = None, - ) -> DppRegistryClient: - """Build a client for a configurable local registry endpoint. - - Defaults to :func:`endpoints.local_registry_base_url`; pass ``base_url`` to override. - """ - resolved = base_url if base_url is not None else endpoints.local_registry_base_url() - return cls(resolved, client=client) - - def health_check(self) -> bool: - """Return ``True`` if the registry service answers ``GET /health`` with a 2xx.""" - return _http.probe_health(self._client, self._base_url) - def close(self) -> None: - """Close the internally created HTTPX client, if this SDK instance owns it.""" if self._owns_client and not self._closed: self._client.close() self._closed = True @@ -57,27 +38,41 @@ def __exit__( ) -> None: self.close() - def post_new_dpp_to_registry(self, request: RegisterDppRequest) -> RegisterDppResponse: + def post_new_dpp_to_registry( + self, request: RegisterDppRequest | dict[str, Any] + ) -> RegisterDppResponse: if request is None: - cause = ValueError("request must not be null") - raise DppMappingClientError( - "DPP registry request could not be mapped before request" - ) from cause - try: - body = request.model_dump_json() - except Exception as exc: # noqa: BLE001 - re-raised as a client error raise DppMappingClientError( "DPP registry request could not be mapped before request" - ) from exc + ) from ValueError("request must not be null") + body = ( + request.model_dump_json(by_alias=True, exclude_none=False) + if isinstance(request, RegisterDppRequest) + else _http.serialize_json(request) + ) response = _http.request_api( - self._client, "POST", _http.resolve(self._base_url, _REGISTER_PATH), body + self._client, "POST", _http.resolve(self._base_url, "/v1/registerDPP"), body ) - payload = _http.require_payload(response) + payload = response.payload + if payload is None: + return RegisterDppResponse(status_code=response.status_code, message=response.message) + if not isinstance(payload, dict): + raise _missing_registry_payload() + registration_id = payload.get("registrationId") + if registration_id is not None and ( + not isinstance(registration_id, str) or not registration_id.strip() + ): + raise _missing_registry_payload() try: - result = RegisterDppResponse.model_validate(payload) + return RegisterDppResponse( + status_code=response.status_code, + message=response.message, + registration_id=registration_id, + ) except ValidationError as exc: raise DppMappingClientError("Registry response could not be mapped") from exc - _http.require_text_field( - {"registrationId": result.registrationId}, "registrationId", "payload.registrationId" - ) - return result + + +def _missing_registry_payload() -> DppMappingClientError: + message = "Missing required response field: payload" + return DppMappingClientError(message) diff --git a/src/dpp_sdk/clients/repo.py b/src/dpp_sdk/clients/repo.py index 904d95b..4ad2636 100644 --- a/src/dpp_sdk/clients/repo.py +++ b/src/dpp_sdk/clients/repo.py @@ -1,73 +1,39 @@ -"""HTTP client for the DPP repository API (port of ``HttpDppRepoClient``). - -Generic over a caller-owned DPP model type ``T``, mediated by a :class:`DppCodec` -(full-payload JSON) and a validator callable. Endpoint paths and the request -pipeline match the Java client exactly. -""" +"""Generic JSON client for the final repository API.""" from __future__ import annotations -import json from datetime import UTC, datetime from types import TracebackType -from typing import Any, Generic, Self, TypeVar +from typing import Any, Self +from urllib.parse import quote import httpx +from pydantic import ValidationError -from . import _http, endpoints -from .errors import DppClientError, DppMappingClientError, DppValidationClientError -from .payloads import CreateDppResponse, DeleteDppResponse, ReadDppIdsRequest, ReadDppIdsResponse +from . import _http +from .errors import DppMappingClientError +from .payloads import ( + CreateDppResponse, + DeleteDppResponse, + DppApiResponse, + DppRepresentation, + ReadDppIdsRequest, + ReadDppIdsResponse, +) -T = TypeVar("T") +_DPPS = "/v1/dpps" -# Canonical repository routes are versioned. The product-ID history route below -# is the one retained, explicitly deprecated compatibility exception. -_DPPS_PATH = "/v1/dpps" -_DPPS_BY_PRODUCT_ID_PATH = "/v1/dppsByProductId/" -_DPPS_BY_ID_AND_DATE_PATH = "/v1/dppsByIdAndDate/" -_DPPS_BY_PRODUCT_ID_AND_DATE_PATH = "/dppsByProductIdAndDate/" -_DPPS_BY_PRODUCT_IDS_PATH = "/v1/dppsByProductIds" +class DppRepoClient: + """Synchronous JSON in/JSON out client for repository operations.""" -class DppRepoClient(Generic[T]): - def __init__( - self, - base_url: str, - codec: _http.DppCodec[T], - validator: _http.DppValidator[T], - *, - client: httpx.Client | None = None, - ) -> None: + def __init__(self, base_url: str, *, client: httpx.Client | None = None) -> None: self._base_url = _http.normalize_base_url(base_url) - self._codec = codec - self._validator = validator self._owns_client = client is None self._client = client if client is not None else _http.build_client() self._closed = False - @classmethod - def for_local_mock( - cls, - codec: _http.DppCodec[T], - validator: _http.DppValidator[T], - *, - base_url: str | None = None, - client: httpx.Client | None = None, - ) -> DppRepoClient[T]: - """Build a client for a configurable local repository endpoint. - - Defaults to :func:`endpoints.local_repo_base_url`; pass ``base_url`` to override. - """ - resolved = base_url if base_url is not None else endpoints.local_repo_base_url() - return cls(resolved, codec, validator, client=client) - - def health_check(self) -> bool: - """Return ``True`` if the repo service answers ``GET /health`` with a 2xx.""" - return _http.probe_health(self._client, self._base_url) - - # --- lifecycle -------------------------------------------------------------- def close(self) -> None: - """Close the internally created HTTPX client, if this SDK instance owns it.""" if self._owns_client and not self._closed: self._client.close() self._closed = True @@ -83,167 +49,164 @@ def __exit__( ) -> None: self.close() - def create_dpp(self, dpp: T) -> CreateDppResponse: - try: - self._validator(dpp) - except DppClientError: - raise - except Exception as exc: # noqa: BLE001 - re-raised as a client error - raise DppValidationClientError("DPP validation failed before request") from exc - body = self._serialize(dpp) - response = self._send("POST", _DPPS_PATH, body) - payload = _http.require_payload(response) - _http.require_text_field(payload, "dppId", "payload.dppId") - return CreateDppResponse.model_validate(payload) - - def read_dpp_by_id(self, dpp_id: str) -> T: - response = self._send("GET", self._full_dpp_path(dpp_id), None) - return self._decode_dpp_payload(response) - - def read_dpp_by_product_id(self, product_id: str) -> T: - path = self._full_product_dpp_path(product_id) - response = self._send("GET", path, None) - return self._decode_dpp_payload(response) - - def read_compressed_dpp_by_id(self, dpp_id: str) -> Any: - response = self._send("GET", self._compressed_dpp_path(dpp_id), None) - return _http.require_payload(response) + def create_dpp( + self, dpp: Any, representation: DppRepresentation | None = DppRepresentation.COMPRESSED + ) -> CreateDppResponse: + representation = _selected(representation) + envelope = self._send( + "POST", _DPPS + representation.query_suffix, _http.serialize_json(_required(dpp)) + ) + payload = envelope.payload + if payload is None: + return CreateDppResponse(status_code=envelope.status_code, message=envelope.message) + if not isinstance(payload, dict): + raise _mapping_error("payload") + identifier = payload.get("digitalProductPassportId") + if identifier is not None and (not isinstance(identifier, str) or not identifier.strip()): + raise _mapping_error("payload.digitalProductPassportId") + return CreateDppResponse( + status_code=envelope.status_code, + message=envelope.message, + digital_product_passport_id=identifier, + ) - def read_dpp_version_by_id_and_date(self, dpp_id: str, date: datetime) -> T: - path = ( - _DPPS_BY_ID_AND_DATE_PATH - + _http.encode_segment(self._require_nonblank(dpp_id, "dppId")) - + "?date=" - + _http.encode_segment(self._instant(date)) - + "&representation=full" + def read_dpp_by_id( + self, dpp_id: str, representation: DppRepresentation | None = DppRepresentation.COMPRESSED + ) -> DppApiResponse: + return self._send( + "GET", self._dpp_path(dpp_id) + _selected(representation).query_suffix, None ) - response = self._send("GET", path, None) - return self._decode_dpp_payload(response) - def read_dpp_version_by_product_id_and_date(self, product_id: str, date: datetime) -> T: - """Read deprecated unversioned product-ID history compatibility route.""" + def read_dpp_by_product_id( + self, + product_id: str, + representation: DppRepresentation | None = DppRepresentation.COMPRESSED, + ) -> DppApiResponse: path = ( - _DPPS_BY_PRODUCT_ID_AND_DATE_PATH - + _http.encode_segment(self._require_nonblank(product_id, "productId")) - + "?date=" - + _http.encode_segment(self._instant(date)) + "/v1/dppsByProductId/" + _segment(product_id) + _selected(representation).query_suffix ) - response = self._send("GET", path, None) - return self._decode_dpp_payload(response) + return self._send("GET", path, None) + + def read_dpp_version_by_id_and_date( + self, + dpp_id: str, + date: datetime, + representation: DppRepresentation | None = DppRepresentation.COMPRESSED, + ) -> DppApiResponse: + if date.tzinfo is None or date.utcoffset() is None: + raise ValueError("date must be timezone-aware") + instant = date.astimezone(UTC) + date_text = instant.isoformat().replace("+00:00", "Z") + query = "?date=" + quote(date_text, safe="") + if _selected(representation) is DppRepresentation.FULL: + query += "&representation=full" + return self._send("GET", "/v1/dppsByIdAndDate/" + _segment(dpp_id) + query, None) def read_dpp_ids_by_product_ids( self, product_ids: list[str], limit: int | None = None, cursor: str | None = None ) -> ReadDppIdsResponse: - request_body = ReadDppIdsRequest(productIdentifiers=product_ids, limit=limit, cursor=cursor) - response = self._send("POST", _DPPS_BY_PRODUCT_IDS_PATH, request_body.model_dump_json()) + request = ReadDppIdsRequest(product_identifiers=product_ids, limit=limit, cursor=cursor) + response = self._send( + "POST", + "/v1/dppsByProductIds", + request.model_dump_json(by_alias=True, exclude_none=False), + ) payload = _http.require_payload(response) _http.require_field(payload, "dppIdentifiers", "payload.dppIdentifiers") - return ReadDppIdsResponse.model_validate(payload) + try: + return ReadDppIdsResponse.model_validate( + {**payload, "statusCode": response.status_code, "message": response.message} + ) + except (ValidationError, TypeError) as exc: + raise DppMappingClientError("DPP identifier response could not be mapped") from exc - def update_dpp_by_id(self, dpp_id: str, partial_dpp: Any) -> T: - body = self._serialize_partial_update(partial_dpp, "update_dpp_by_id") - response = self._send("PATCH", self._dpp_path(dpp_id), body) - return self._decode_dpp_payload(response) + def update_dpp_by_id( + self, + dpp_id: str, + partial_dpp: Any, + representation: DppRepresentation | None = DppRepresentation.COMPRESSED, + ) -> DppApiResponse: + body = _http.serialize_json(_required(partial_dpp)) + return self._send( + "PATCH", self._dpp_path(dpp_id) + _selected(representation).query_suffix, body + ) def delete_dpp_by_id(self, dpp_id: str) -> DeleteDppResponse: response = self._send("DELETE", self._dpp_path(dpp_id), None) - return DeleteDppResponse(statusCode=response.statusCode, messages=response.messages) + return DeleteDppResponse(status_code=response.status_code, message=response.message) - # --- fine-granular elements ------------------------------------------------- - def read_data_element(self, dpp_id: str, element_path: str) -> Any: - response = self._send("GET", self._data_element_path(dpp_id, element_path), None) - return _http.require_payload(response) - - def update_data_element(self, dpp_id: str, element_path: str, payload: Any) -> Any: - body = self._serialize_partial_update(payload, "update_data_element") - response = self._send("PATCH", self._data_element_path(dpp_id, element_path), body) - return _http.require_payload(response) + def read_data_element( + self, + dpp_id: str, + element_id_path: str, + representation: DppRepresentation | None = DppRepresentation.COMPRESSED, + ) -> DppApiResponse: + return self._element_request("GET", dpp_id, element_id_path, None, representation) - # --- internals -------------------------------------------------------------- - def _send(self, method: str, path: str, body: str | None) -> Any: - return _http.request_api(self._client, method, _http.resolve(self._base_url, path), body) + def update_data_element( + self, + dpp_id: str, + element_id_path: str, + data_element: Any, + representation: DppRepresentation = DppRepresentation.COMPRESSED, + ) -> DppApiResponse: + return self._element_request( + "PATCH", + dpp_id, + element_id_path, + _http.serialize_json(_required(data_element)), + representation, + ) - def _serialize(self, dpp: T) -> str: - try: - json_str = self._codec.to_json(dpp) - if json_str is None: - raise ValueError("codec returned null JSON") - except DppClientError: - raise - except Exception as exc: # noqa: BLE001 - re-raised as a client error - raise DppMappingClientError("DPP serialization failed before request") from exc - return json_str + def _element_request( + self, + method: str, + dpp_id: str, + element_id_path: str, + body: str | None, + representation: DppRepresentation | None, + ) -> DppApiResponse: + if not element_id_path.strip(): + raise ValueError("elementIdPath must not be blank") + response = self._send( + method, + self._dpp_path(dpp_id) + + "/elements/" + + _segment(element_id_path) + + _selected(representation).query_suffix, + body, + ) + _http.require_payload(response) + return response + + def _send(self, method: str, path: str, body: str | None) -> DppApiResponse: + return _http.request_api( + self._client, + method, + _http.resolve(self._base_url, path), + body, + allow_no_content=True, + ) @staticmethod - def _serialize_partial_update(payload: Any, operation: str) -> str: - """Encode a partial-update body as strict interoperable JSON before I/O.""" - try: - return json.dumps(payload, allow_nan=False) - except (TypeError, ValueError) as exc: - raise DppMappingClientError( - f"{operation} payload serialization failed before request" - ) from exc + def _dpp_path(dpp_id: str) -> str: + return _DPPS + "/" + _segment(dpp_id) - def _decode_dpp_payload(self, response: Any) -> T: - payload = _http.require_payload(response) - payload_json = json.dumps(payload) - try: - result = self._codec.from_json(payload_json) - if result is None: - raise ValueError("codec returned null DPP") - return result - except DppClientError: - raise - except Exception as exc: # noqa: BLE001 - re-raised as a client error - raise DppMappingClientError("DPP deserialization failed after response") from exc - @staticmethod - def _dpp_path(dpp_id: str) -> str: - return ( - _DPPS_PATH - + "/" - + _http.encode_segment(DppRepoClient._require_nonblank(dpp_id, "dppId")) - ) +def _required(value: Any) -> Any: + if value is None: + raise TypeError("JSON value must not be null") + return value - @classmethod - def _full_dpp_path(cls, dpp_id: str) -> str: - return cls._dpp_path(dpp_id) + "?representation=full" - @classmethod - def _compressed_dpp_path(cls, dpp_id: str) -> str: - return cls._dpp_path(dpp_id) + "?representation=compressed" +def _segment(value: str) -> str: + return _http.encode_segment(value) - @classmethod - def _full_product_dpp_path(cls, product_id: str) -> str: - return ( - _DPPS_BY_PRODUCT_ID_PATH - + _http.encode_segment(cls._require_nonblank(product_id, "productId")) - + "?representation=full" - ) - @classmethod - def _data_element_path(cls, dpp_id: str, element_path: str) -> str: - return ( - cls._dpp_path(dpp_id) - + "/elements/" - + _http.encode_segment(cls._require_nonblank(element_path, "elementPath")) - ) +def _selected(representation: DppRepresentation | None) -> DppRepresentation: + return representation or DppRepresentation.COMPRESSED - @staticmethod - def _require_nonblank(value: str, field_name: str) -> str: - if not value or not value.strip(): - raise ValueError(f"{field_name} must not be blank") - return value - @staticmethod - def _instant(value: datetime) -> str: - if value.tzinfo is None or value.utcoffset() is None: - raise ValueError("date must be timezone-aware") - instant = value.astimezone(UTC) - if instant.microsecond == 0: - timespec = "seconds" - elif instant.microsecond % 1000 == 0: - timespec = "milliseconds" - else: - timespec = "microseconds" - return instant.isoformat(timespec=timespec).replace("+00:00", "Z") +def _mapping_error(path: str) -> DppMappingClientError: + message = f"Missing required response field: {path}" + return DppMappingClientError(message) diff --git a/src/dpp_sdk/core/README.md b/src/dpp_sdk/core/README.md index 0184ed9..9702e06 100644 --- a/src/dpp_sdk/core/README.md +++ b/src/dpp_sdk/core/README.md @@ -1,132 +1,11 @@ # DPP SDK core module -## Purpose - -`dpp_sdk.core` is the reusable part of the SDK. Use it when you need the common passport details: -identity, nameplate information, organisations and contacts, documentation, and validation. The -furniture-specific DPP4Fun module builds on these models. Generic clients can work with any model -that an application knows how to convert and check. - -It does not provide a standalone JSON codec, furniture-specific models, HTTP clients, repository or -registry services, persistence, or lifecycle-event storage. - -## Architecture at a glance - -![Core model boundary](../../../docs/architecture/python-core-model.svg) - -`dpp_sdk.core` provides reusable frozen DPP models, explicit semantic validation, and shared -errors. It does not depend on DPP4Fun or the HTTP clients. - -All public models are frozen Pydantic models. Construction checks the shape of the data; call -`validate_dpp_core()` when you want to check the business rules as well. - -## Public surface - -| Need | Public types or function | -| --- | --- | -| Passport identity and update dates | `PassportMetadata` | -| Product identity and economic operators | `Nameplate`, `Organization`, `OrganizationRole` | -| Contact details | `Contact`, `Address`, `Email`, `Telephone` | -| Optional user documentation | `Documentation` | -| Reusable aggregate | `DppCore`, `Dpp` | -| Explicit semantic checks | `validate_dpp_core()` | -| Shared failures | `DppError`, `DppValidationError`, `DppMappingError` | - -The stable identifiers are exposed by a concrete `Dpp` aggregate: `dpp_id` is the string form of -`uniqueProductIdentifier`; `product_id` is `gtinCode`. - -For the canonical field/default/null table, use the repository [model guide](../../../docs/model-guide.md). -For semantic validator order, null behavior, and error paths, use the [validation-rule reference] -(../../../docs/validation-rules.md). - -## Practical usage - -```python -from datetime import date -from uuid import UUID - -from dpp_sdk.core import ( - DppCore, - Nameplate, - Organization, - OrganizationRole, - PassportMetadata, - validate_dpp_core, -) - -core = DppCore( - passportMetadata=PassportMetadata( - uniqueProductIdentifier=UUID("11111111-1111-1111-1111-111111111111"), - passportUpdateDates=(date(2026, 6, 29),), - ), - nameplate=Nameplate( - gtinCode="04012345678901", - manufacturer=Organization( - name="Cir4Fun Furniture GmbH", - role=OrganizationRole.MANUFACTURER, - ), - ), -) -validate_dpp_core(core) -``` - -Construction rejects structurally invalid data such as missing required fields, unknown fields, -blank required text, blank optional text when present, and negative count values. Semantic -validation is fail-fast and raises `DppValidationError` for the first applicable rule breach. - -## Immutable updates - -Use `with_updates()` to create a structurally revalidated replacement; it never mutates the -original model: - -```python -updated_nameplate = core.nameplate.with_updates(gtinCode="04012345678902") -updated_core = core.with_updates(nameplate=updated_nameplate) -assert core.nameplate.gtinCode == "04012345678901" -assert updated_core.nameplate.gtinCode == "04012345678902" -``` - -Do not use unchecked `model_copy(update=...)` as a public update mechanism, because it can bypass -the normal structural construction boundary. - -## Validation and transport boundary - -Core model fields retain their camelCase JSON names. UUID values serialize canonically and dates -serialize as ISO dates. The aggregate-level DPP4Fun codec owns flattened versus nested JSON -transport; there is no standalone core JSON codec. - -Required text must not be null or contract-blank. Optional text can be null but cannot be -contract-blank when present; accepted text is not trimmed. The contract uses a frozen Unicode -whitespace table: U+200B remains visible text rather than blank. For aggregate, numeric, list, BOM, -and codec rules, see the [DPP4Fun module](../dpp4fun/README.md). - -## Build and test locally - -**Purpose:** run focused core model/validator checks. **Run from:** repository root. -**Prerequisites:** the checkout development environment. The [release guide](../../../RELEASING.md) -owns installation, full validation, builds, archive inspection, and cleanup. - -### PowerShell - -```powershell -.\.venv\Scripts\python.exe -m pytest tests/test_core_model.py tests/test_core_validation.py -``` - -### Linux/macOS - -```bash -.venv/bin/python -m pytest tests/test_core_model.py tests/test_core_validation.py -``` - -**Expected result:** focused structural and semantic core tests pass. **Cleanup:** none. - -## Boundaries and limitations - -- No concrete DPP aggregate is supplied by this module; use DPP4Fun or another consumer-owned - aggregate over `Dpp`. -- No semantic validation runs implicitly during every parse or update. Call the explicit validator - when semantic validity is required. -- No HTTP, service, persistence, Docker, Spring, EDC, or standards-certification capability is - provided here. - -Next: [DPP4Fun module](../dpp4fun/README.md) or [SDK usage](../../../docs/usage.md). +`dpp_sdk.core` holds the general passport model (`DigitalProductPassport`, `DppHeader`, and five +DataElement types), FULL and COMPRESSED JSON conversion (`DppJsonCodec`), and validation +(`DppValidator`, `DataFormatValidator`, `IdentifierValidator`). It does not depend on DPP4Fun or +the HTTP clients. + +- [Model guide](../../../docs/model-guide.md): build, read, edit, validate, and convert passports. +- [Building your own profile](../../../docs/extending.md): use Core for other product groups. +- [Standards alignment and validation rules](../../../docs/conformance.md). +- [Contributing](../../../CONTRIBUTING.md): tests and checks for this module. diff --git a/src/dpp_sdk/core/__init__.py b/src/dpp_sdk/core/__init__.py index 39dcf75..2d4abfe 100644 --- a/src/dpp_sdk/core/__init__.py +++ b/src/dpp_sdk/core/__init__.py @@ -1,38 +1,72 @@ -"""Reusable core DPP model, validation, and identifiers. - -Reusable core DPP model, validation, and identifier contracts. -""" +"""Reusable core passport models, transport, and semantic validation.""" from .errors import DppError, DppMappingError, DppValidationError +from .json_codec import ( + CompressedElementExpansionResult, + CompressedExpansionResult, + DppJsonCodec, + DppJsonException, + ExpansionIssue, +) from .model import ( - Address, - Contact, - Documentation, - Dpp, - DppCore, - Email, - Nameplate, - Organization, - OrganizationRole, - PassportMetadata, - Telephone, + DataElement, + DataElementCollection, + DigitalProductPassport, + DppHeader, + Granularity, + MultiLanguageDataElement, + MultiLanguageValue, + MultiValuedDataElement, + RelatedResource, + SingleValuedDataElement, +) +from .semantic import DataElementKind, SemanticDefinition, SemanticPath, SemanticResolver +from .validation import ( + DataFormatValidator, + DppValidator, + EconomicOperatorIdentifierMethod, + FacilityIdentifierMethod, + IdentifierValidationContext, + IdentifierValidator, + ProductIdentifierMethod, + ValidationIssue, + ValidationResult, + ValidationSeverity, + ValidationStatus, ) -from .validation import validate_dpp_core __all__ = [ - "Address", - "Contact", - "Documentation", - "Dpp", - "DppCore", + "CompressedElementExpansionResult", + "CompressedExpansionResult", + "DataElement", + "DataElementCollection", + "DataElementKind", + "DataFormatValidator", + "DigitalProductPassport", "DppError", + "DppHeader", + "DppJsonCodec", + "DppJsonException", "DppMappingError", "DppValidationError", - "Email", - "Nameplate", - "Organization", - "OrganizationRole", - "PassportMetadata", - "Telephone", - "validate_dpp_core", + "DppValidator", + "EconomicOperatorIdentifierMethod", + "ExpansionIssue", + "FacilityIdentifierMethod", + "Granularity", + "IdentifierValidationContext", + "IdentifierValidator", + "MultiLanguageDataElement", + "MultiLanguageValue", + "MultiValuedDataElement", + "ProductIdentifierMethod", + "RelatedResource", + "SemanticDefinition", + "SemanticPath", + "SemanticResolver", + "SingleValuedDataElement", + "ValidationIssue", + "ValidationResult", + "ValidationSeverity", + "ValidationStatus", ] diff --git a/src/dpp_sdk/core/json_codec.py b/src/dpp_sdk/core/json_codec.py new file mode 100644 index 0000000..3ac4280 --- /dev/null +++ b/src/dpp_sdk/core/json_codec.py @@ -0,0 +1,987 @@ +"""Canonical FULL and COMPRESSED JSON transport for the shared DPP model.""" + +from __future__ import annotations + +import json +import math +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from datetime import UTC, datetime +from decimal import Decimal +from typing import Any, TypeAlias, cast + +from .errors import DppMappingError +from .model import ( + DataElement, + DataElementCollection, + DigitalProductPassport, + DppHeader, + Granularity, + MultiLanguageDataElement, + MultiLanguageValue, + MultiValuedDataElement, + RelatedResource, + SingleValuedDataElement, +) +from .semantic import DataElementKind, SemanticDefinition, SemanticPath, SemanticResolver + +_DataElementModel: TypeAlias = ( + DataElementCollection + | SingleValuedDataElement + | MultiValuedDataElement + | MultiLanguageDataElement + | RelatedResource +) + + +class DppJsonException(DppMappingError): # noqa: N818 - preserve the Java public type name + """Raised when canonical DPP JSON cannot be written or interpreted.""" + + +@dataclass(frozen=True) +class ExpansionIssue: + path: str + reason: str + + @property + def rule(self) -> str: + return "EN18223_COMPRESSED_SEMANTICS_UNRESOLVED" + + @property + def severity(self) -> str: + return "WARNING" + + +@dataclass(frozen=True) +class CompressedExpansionResult: + passport: DigitalProductPassport + issues: tuple[ExpansionIssue, ...] = () + + +@dataclass(frozen=True) +class CompressedElementExpansionResult: + element: _DataElementModel + issues: tuple[ExpansionIssue, ...] = () + + +_HEADER_FIELDS = ( + "digitalProductPassportId", + "uniqueProductIdentifier", + "granularity", + "dppSchemaVersion", + "dppStatus", + "lastUpdated", + "economicOperatorId", + "facilityId", + "contentSpecificationIds", +) +_HEADER_ATTRS = { + "digitalProductPassportId": "digital_product_passport_id", + "uniqueProductIdentifier": "unique_product_identifier", + "granularity": "granularity", + "dppSchemaVersion": "dpp_schema_version", + "dppStatus": "dpp_status", + "lastUpdated": "last_updated", + "economicOperatorId": "economic_operator_id", + "facilityId": "facility_id", + "contentSpecificationIds": "content_specification_ids", +} +_RESOURCE_FIELDS = {"contentType", "url", "language", "resourceTitle"} + + +def _object_pairs(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + result: dict[str, Any] = {} + for key, value in pairs: + if key in result: + raise ValueError(f"duplicate JSON property '{key}'") + result[key] = value + return result + + +def _parse_json(text: str) -> Any: + def reject_constant(value: str) -> None: + raise ValueError(f"non-standard JSON number {value}") + + return json.loads( + text, + object_pairs_hook=_object_pairs, + parse_float=Decimal, + parse_constant=reject_constant, + ) + + +def _dump_json(value: Any) -> str: + if value is None or isinstance(value, (str, bool, int)): + return json.dumps(value, ensure_ascii=False, allow_nan=False, separators=(",", ":")) + if isinstance(value, Decimal): + if not value.is_finite(): + raise DppJsonException("number must be finite") + return str(value) + if isinstance(value, float): + if not math.isfinite(value): + raise DppJsonException("number must be finite") + return json.dumps(value, allow_nan=False, separators=(",", ":")) + if isinstance(value, Mapping): + return ( + "{" + + ",".join(_dump_json(str(key)) + ":" + _dump_json(item) for key, item in value.items()) + + "}" + ) + if isinstance(value, (tuple, list)): + return "[" + ",".join(_dump_json(item) for item in value) + "]" + raise DppJsonException(f"unsupported JSON value type {type(value).__name__}") + + +def _ordered_json(value: Any) -> Any: + if isinstance(value, Mapping): + return {key: _ordered_json(value[key]) for key in sorted(value)} + if isinstance(value, (tuple, list)): + return [_ordered_json(item) for item in value] + return value + + +def _required_text(obj: Mapping[str, Any], field: str, path: str) -> str: + value = obj.get(field) + if not isinstance(value, str): + raise DppJsonException(f"{path}.{field} must be a string") + return value + + +def _optional_text(obj: Mapping[str, Any], field: str, path: str) -> str | None: + value = obj.get(field) + if value is None: + return None + if not isinstance(value, str): + raise DppJsonException(f"{path}.{field} must be a string when present") + return value + + +def _instant_text(value: datetime) -> str: + utc_value = value.astimezone(UTC) + text = utc_value.strftime("%Y-%m-%dT%H:%M:%S") + micros = utc_value.microsecond + if micros: + fraction = f"{micros // 1000:03d}" if micros % 1000 == 0 else f"{micros:06d}" + text += f".{fraction}" + return text + "Z" + + +class DppJsonCodec: + """Translate canonical FULL and operational COMPRESSED representations.""" + + def to_expanded_json(self, dpp: DigitalProductPassport) -> str: + try: + root = self._header_json(dpp.dpp_header) + root["elements"] = [ + self._expanded_element(element, "$.elements") for element in dpp.data_elements + ] + return _dump_json(root) + except DppJsonException: + raise + except Exception as exc: + raise DppJsonException("Failed to serialize expanded DPP JSON") from exc + + def from_expanded_json(self, json_text: str) -> DigitalProductPassport: + try: + root = _parse_json(json_text) + if not isinstance(root, dict): + raise DppJsonException("Expanded DPP JSON root must be an object") + self._known(root, {*_HEADER_FIELDS, "elements"}, "$") + header = self._read_header(root) + elements_value = root.get("elements", []) + if not isinstance(elements_value, list): + raise DppJsonException("$.elements must be an array") + elements = tuple( + self._read_element(value, f"$.elements[{index}]") + for index, value in enumerate(elements_value) + ) + return DigitalProductPassport(dpp_header=header, data_elements=elements) + except DppJsonException: + raise + except Exception as exc: + raise DppJsonException("Failed to deserialize expanded DPP JSON") from exc + + def read_header(self, dpp_json: str | Mapping[str, Any]) -> DppHeader: + """Read only flattened header fields from FULL or COMPRESSED JSON.""" + try: + root = _parse_json(dpp_json) if isinstance(dpp_json, str) else dpp_json + if not isinstance(root, Mapping): + raise DppJsonException("$ DPP JSON root must be an object") + return self._read_header(root) + except DppJsonException: + raise + except Exception as exc: + raise DppJsonException("Failed to read DPP header") from exc + + def to_compressed_json(self, dpp: DigitalProductPassport) -> str: + try: + root = self._header_json(dpp.dpp_header) + for element in dpp.data_elements: + element_id = self._required_id(element, "$") + if element_id in _HEADER_FIELDS: + raise DppJsonException( + f"$.{element_id} compressed element key collides with " + "an existing root property" + ) + root[element_id] = self._compressed_value(element, f"$.{element_id}") + return _dump_json(root) + except DppJsonException: + raise + except Exception as exc: + raise DppJsonException("Failed to serialize compressed DPP JSON") from exc + + def expand_compressed_json( + self, + json_text: str, + previous: DigitalProductPassport | None = None, + resolver: SemanticResolver | None = None, + ) -> CompressedExpansionResult: + """Expand operational JSON, optionally using prior values and a resolver.""" + try: + root = _parse_json(json_text) + if not isinstance(root, dict): + raise DppJsonException("$ compressed DPP JSON root must be an object") + header = self._read_header(root) + old = ( + {element.element_id: element for element in previous.data_elements} + if previous + else {} + ) + issues: list[ExpansionIssue] = [] + elements: list[_DataElementModel] = [] + for key, value in root.items(): + if key in _HEADER_FIELDS: + continue + path = f"$.'{key}'" + elements.append( + self._expand_element( + key, + value, + path, + SemanticPath.of(key), + header.content_specification_ids, + resolver, + old.get(key), + issues, + ) + ) + return CompressedExpansionResult( + DigitalProductPassport(dpp_header=header, data_elements=tuple(elements)), + tuple(issues), + ) + except DppJsonException: + raise + except Exception as exc: + raise DppJsonException("Failed to expand compressed DPP JSON") from exc + + def expand_compressed_json_with_resolver( + self, json_text: str, resolver: SemanticResolver + ) -> CompressedExpansionResult: + return self.expand_compressed_json(json_text, resolver=resolver) + + def to_compressed_element_json(self, element: DataElement) -> str: + return _dump_json(self._compressed_value(element, f"$.'{self._required_id(element, '$')}'")) + + def to_expanded_element_json(self, element: DataElement) -> str: + return _dump_json(self._expanded_element(element, "$element")) + + def from_expanded_element_json(self, json_text: str) -> _DataElementModel: + try: + return self._read_element(_parse_json(json_text), "$element") + except DppJsonException: + raise + except Exception as exc: + raise DppJsonException("Failed to parse expanded DataElement JSON") from exc + + def expand_compressed_element_json( + self, element_id: str, json_text: str, previous: DataElement | None = None + ) -> _DataElementModel: + return self.expand_compressed_element_json_with_issues( + element_id, json_text, previous + ).element + + def expand_compressed_element_json_with_issues( + self, + element_id: str, + json_text: str, + previous: DataElement | None = None, + content_specification_ids: Sequence[str] = (), + resolver: SemanticResolver | None = None, + semantic_path: SemanticPath | None = None, + ) -> CompressedElementExpansionResult: + if not element_id.strip(): + raise DppJsonException("$element elementId must not be blank") + path = semantic_path or SemanticPath.of(element_id) + try: + issues: list[ExpansionIssue] = [] + element = self._expand_element( + element_id, + _parse_json(json_text), + f"$.'{element_id}'", + path, + tuple(content_specification_ids), + resolver, + previous, + issues, + ) + return CompressedElementExpansionResult(element, tuple(issues)) + except DppJsonException: + raise + except Exception as exc: + raise DppJsonException("Failed to parse compressed DataElement JSON") from exc + + def _header_json(self, header: DppHeader) -> dict[str, Any]: + root: dict[str, Any] = {} + for field in _HEADER_FIELDS: + value = getattr(header, _HEADER_ATTRS[field]) + if field == "facilityId" and value is None: + continue + if value is None: + if field == "granularity": + raise DppJsonException("$.granularity is required") + if field == "lastUpdated": + raise DppJsonException("$.lastUpdated is required") + if field == "contentSpecificationIds": + value = () + else: + raise DppJsonException(f"$.{field} is required") + if field == "granularity": + value = value.value + elif field == "lastUpdated": + value = _instant_text(value) + elif field == "contentSpecificationIds": + value = list(value) + root[field] = value + return root + + def _read_header(self, root: Mapping[str, Any]) -> DppHeader: + values: dict[str, Any] = {} + for field in ( + "digitalProductPassportId", + "uniqueProductIdentifier", + "dppSchemaVersion", + "dppStatus", + "economicOperatorId", + ): + values[_HEADER_ATTRS[field]] = _required_text(root, field, "$") + granularity_text = _required_text(root, "granularity", "$") + try: + values["granularity"] = Granularity(granularity_text.lower()) + except ValueError as exc: + raise DppJsonException( + f"$.granularity has an unknown value '{granularity_text}'" + ) from exc + timestamp = _required_text(root, "lastUpdated", "$") + try: + parsed = datetime.fromisoformat(timestamp.replace("Z", "+00:00")) + if parsed.tzinfo is None: + raise ValueError + if parsed.utcoffset() != UTC.utcoffset(parsed): + raise ValueError + except ValueError as exc: + raise DppJsonException( + "$.lastUpdated must be a canonical UTC ISO-8601 instant" + ) from exc + canonical = _instant_text(parsed) + if canonical != timestamp: + raise DppJsonException( + "$.lastUpdated must use canonical UTC ISO-8601 Instant representation" + ) + values["last_updated"] = parsed + values["facility_id"] = _optional_text(root, "facilityId", "$") + specs = root.get("contentSpecificationIds", []) + if not isinstance(specs, list) or any(not isinstance(item, str) for item in specs): + raise DppJsonException("$.contentSpecificationIds must be an array of strings") + values["content_specification_ids"] = tuple(specs) + return DppHeader(**values) + + def _expanded_element(self, element: DataElement, path: str) -> dict[str, Any]: + element_id = self._required_id(element, path) + result: dict[str, Any] = {"elementId": element_id, "objectType": element.object_type} + if element.dictionary_reference is not None: + result["dictionaryReference"] = element.dictionary_reference + if isinstance(element, DataElementCollection): + if not element.elements: + raise DppJsonException(f"{path} must contain at least one item") + result["elements"] = [ + self._expanded_element(child, path + ".elements") for child in element.elements + ] + elif isinstance(element, SingleValuedDataElement): + if element.value_data_type is not None: + result["valueDataType"] = element.value_data_type + result["value"] = _ordered_json(element.value) + elif isinstance(element, MultiValuedDataElement): + if not element.value: + raise DppJsonException(f"{path}.value must contain at least one item") + if element.value_data_type is not None: + result["valueDataType"] = element.value_data_type + result["value"] = [ + self._expanded_element(child, path + ".value") for child in element.value + ] + elif isinstance(element, MultiLanguageDataElement): + if not element.value: + raise DppJsonException(f"{path}.value must contain at least one item") + result["value"] = [self._lang_value(item) for item in element.value] + elif isinstance(element, RelatedResource): + for field, value in (("contentType", element.content_type), ("url", element.url)): + if value is None: + raise DppJsonException(f"{path}.{field} is required") + result[field] = value + if element.language is not None: + result["language"] = element.language + if element.resource_title is not None: + result["resourceTitle"] = element.resource_title + else: + raise DppJsonException(f"{path} has an unsupported DataElement subtype") + return result + + @staticmethod + def _lang_value(value: MultiLanguageValue) -> dict[str, Any]: + if value.value is None or value.language is None: + raise DppJsonException("multilingual value and language are required") + return {"value": value.value, "language": value.language} + + def _compressed_value(self, element: DataElement, path: str) -> Any: + if isinstance(element, SingleValuedDataElement): + value = _ordered_json(element.value) + if isinstance(value, float) and not math.isfinite(value): + raise DppJsonException(f"{path} number must be finite") + return value + if isinstance(element, DataElementCollection): + if not element.elements: + raise DppJsonException(f"{path} must contain at least one item") + result: dict[str, Any] = {} + for child in element.elements: + child_id = self._required_id(child, path) + if child_id in result: + raise DppJsonException(f"{path}.{child_id} duplicate compressed element key") + result[child_id] = self._compressed_value(child, path + "." + child_id) + return result + if isinstance(element, MultiValuedDataElement): + if not element.value: + raise DppJsonException(f"{path} must contain at least one item") + values = [ + self._compressed_value(child, f"{path}[{i}]") + for i, child in enumerate(element.value) + ] + if any(_json_kind(value) != _json_kind(values[0]) for value in values): + raise DppJsonException( + f"{path} MultiValuedDataElement items must have the same JSON datatype" + ) + return values + if isinstance(element, MultiLanguageDataElement): + if not element.value: + raise DppJsonException(f"{path} must contain at least one item") + return [self._lang_value(item) for item in element.value] + if isinstance(element, RelatedResource): + if element.content_type is None or element.url is None: + raise DppJsonException(f"{path} RelatedResource requires contentType and url") + result = {"contentType": element.content_type, "url": element.url} + if element.resource_title is not None: + result = {"resourceTitle": element.resource_title, **result} + if element.language is not None: + result["language"] = element.language + return result + raise DppJsonException(f"{path} has an unsupported DataElement subtype") + + def _read_element(self, value: Any, path: str) -> _DataElementModel: + if not isinstance(value, dict): + raise DppJsonException(f"{path} must be an object") + element_id = _required_text(value, "elementId", path) + kind = _required_text(value, "objectType", path) + dictionary = _optional_text(value, "dictionaryReference", path) + common = {"elementId", "objectType", "dictionaryReference"} + if kind == "DataElementCollection": + self._known(value, common | {"elements"}, path) + children = value.get("elements") + if not isinstance(children, list) or not children: + raise DppJsonException(f"{path}.elements must contain at least one item") + return DataElementCollection( + element_id=element_id, + dictionary_reference=dictionary, + elements=tuple( + self._read_element(item, f"{path}.elements[{i}]") + for i, item in enumerate(children) + ), + ) + if kind == "SingleValuedDataElement": + self._known(value, common | {"valueDataType", "value"}, path) + if "value" not in value: + raise DppJsonException(f"{path}.value is required") + return SingleValuedDataElement( + element_id=element_id, + dictionary_reference=dictionary, + value_data_type=_optional_text(value, "valueDataType", path), + value=value["value"], + ) + if kind == "MultiValuedDataElement": + self._known(value, common | {"valueDataType", "value", "elements"}, path) + if ("value" in value) == ("elements" in value): + raise DppJsonException( + f"{path} MultiValuedDataElement must contain exactly one of " + "'elements' or 'value'" + ) + items = value.get("value", value.get("elements")) + if not isinstance(items, list) or not items: + raise DppJsonException(f"{path}.value must contain at least one item") + return MultiValuedDataElement( + element_id=element_id, + dictionary_reference=dictionary, + value_data_type=_optional_text(value, "valueDataType", path), + value=tuple( + self._read_element(item, f"{path}.value[{i}]") for i, item in enumerate(items) + ), + ) + if kind == "MultiLanguageDataElement": + self._known(value, common | {"value"}, path) + items = value.get("value") + if not isinstance(items, list) or not items: + raise DppJsonException(f"{path}.value must contain at least one item") + entries = [] + for i, item in enumerate(items): + item_path = f"{path}.value[{i}]" + if not isinstance(item, dict): + raise DppJsonException(f"{item_path} must be an object") + self._known(item, {"value", "language"}, item_path) + entries.append( + MultiLanguageValue( + value=_required_text(item, "value", item_path), + language=_required_text(item, "language", item_path), + ) + ) + return MultiLanguageDataElement( + element_id=element_id, dictionary_reference=dictionary, value=tuple(entries) + ) + if kind == "RelatedResource": + self._known(value, common | _RESOURCE_FIELDS, path) + return RelatedResource( + element_id=element_id, + dictionary_reference=dictionary, + content_type=_required_text(value, "contentType", path), + url=_required_text(value, "url", path), + language=_optional_text(value, "language", path), + resource_title=_optional_text(value, "resourceTitle", path), + ) + raise DppJsonException(f"{path}.objectType unknown objectType '{kind}'") + + @staticmethod + def _known(value: Mapping[str, Any], allowed: set[str], path: str) -> None: + for field in value: + if field not in allowed: + raise DppJsonException(f"{path}.{field} unknown property") + + @staticmethod + def _required_id(element: DataElement, path: str) -> str: + if element.element_id is None: + raise DppJsonException(f"{path} elementId is required") + return element.element_id + + def _expand_element( + self, + element_id: str, + value: Any, + path: str, + semantic_path: SemanticPath, + content_specification_ids: Sequence[str], + resolver: SemanticResolver | None, + previous: DataElement | None, + issues: list[ExpansionIssue], + ) -> _DataElementModel: + if not element_id.strip(): + raise DppJsonException(f"{path} elementId must not be blank") + if value is None: + raise DppJsonException(f"{path} null cannot form a required DataElement value") + definition = ( + resolver.resolve(content_specification_ids, semantic_path) if resolver else None + ) + if definition is not None: + self._check_definition(element_id, definition, previous, path) + kind = definition.kind if definition else None + if kind is not None: + assert definition is not None + retained_single_value = False + value_type_missing = False + if ( + kind is None + and isinstance(previous, SingleValuedDataElement) + and isinstance(value, (dict, list)) + ): + expanded: _DataElementModel = previous.with_updates(value=value) + retained_single_value = True + issues.append( + ExpansionIssue( + path, + "object or array subtype was retained from the stored single-valued element", + ) + ) + elif kind is DataElementKind.SINGLE: + assert definition is not None + value_type_missing = True + expanded = SingleValuedDataElement( + element_id=definition.element_id or element_id, + dictionary_reference=definition.dictionary_reference, + value_data_type=definition.value_data_type, + value=value, + ) + elif kind is DataElementKind.COLLECTION or ( + kind is None + and isinstance(value, dict) + and value + and (not self._looks_resource(value) or isinstance(previous, DataElementCollection)) + ): + if not isinstance(value, dict) or not value: + raise DppJsonException( + f"{path} semantic definition expects a non-empty DataElementCollection object" + ) + if kind is None: + issues.append( + ExpansionIssue( + path, + "DataElementCollection was inferred from an object; " + "an object-valued single element is also possible", + ) + ) + old_children = ( + {child.element_id: child for child in previous.elements} + if isinstance(previous, DataElementCollection) + else {} + ) + children = tuple( + self._expand_element( + key, + child_value, + path + ".'" + key + "'", + semantic_path.child(key), + content_specification_ids, + resolver, + old_children.get(key), + issues, + ) + for key, child_value in value.items() + ) + expanded = DataElementCollection( + element_id=definition.element_id if definition else element_id, + dictionary_reference=definition.dictionary_reference if definition else None, + elements=children, + ) + elif kind is DataElementKind.RELATED_RESOURCE or ( + kind is None + and isinstance(value, dict) + and self._looks_resource(value) + and not isinstance(previous, DataElementCollection) + ): + if kind is not None and ( + not isinstance(value, dict) or "contentType" not in value or "url" not in value + ): + raise DppJsonException( + f"{path} semantic definition expects a RelatedResource object " + "with contentType and url" + ) + if set(value) - _RESOURCE_FIELDS: + if kind is not None: + raise DppJsonException( + f"{path} resolved RelatedResource contains an unsupported field" + ) + issues.append( + ExpansionIssue( + path, + "resource-like object has additional fields; " + "retained as a single JSON value", + ) + ) + expanded = SingleValuedDataElement(element_id=element_id, value=value) + else: + expanded = RelatedResource( + element_id=definition.element_id if definition else element_id, + dictionary_reference=definition.dictionary_reference if definition else None, + content_type=_required_text(value, "contentType", path), + url=_required_text(value, "url", path), + language=_optional_text(value, "language", path), + resource_title=_optional_text(value, "resourceTitle", path), + ) + if kind is None: + issues.append( + ExpansionIssue( + path, + "RelatedResource was inferred from contentType/url; " + "an object-valued single element is also possible", + ) + ) + elif kind is None and isinstance(value, dict) and not value: + expanded = SingleValuedDataElement(element_id=element_id, value=value) + issues.append( + ExpansionIssue( + path, + "empty object inferred as a single value because a collection " + "requires an element", + ) + ) + elif kind in (DataElementKind.MULTI_VALUED, DataElementKind.MULTI_LANGUAGE) or isinstance( + value, list + ): + if not isinstance(value, list) or not value: + if kind is not None: + raise DppJsonException(f"{path} expected a non-empty array") + expanded = SingleValuedDataElement(element_id=element_id, value=value) + if not value: + issues.append( + ExpansionIssue( + path, + "empty array inferred as a single value because a multi-valued " + "element requires an item", + ) + ) + elif kind is DataElementKind.MULTI_LANGUAGE or ( + kind is None + and not isinstance(previous, MultiValuedDataElement) + and self._is_language_array(value) + ): + entries = tuple( + MultiLanguageValue( + value=_required_text(item, "value", f"{path}[{i}]"), + language=_required_text(item, "language", f"{path}[{i}]"), + ) + for i, item in enumerate(value) + if isinstance(item, dict) + ) + if len(entries) != len(value): + if kind is not None: + raise DppJsonException( + f"{path} semantic definition expects language/value array items" + ) + expanded = SingleValuedDataElement(element_id=element_id, value=value) + else: + expanded = MultiLanguageDataElement( + element_id=definition.element_id if definition else element_id, + dictionary_reference=definition.dictionary_reference + if definition + else None, + value=entries, + ) + if kind is None: + issues.append( + ExpansionIssue( + path, + "MultiLanguageDataElement was inferred from language/value items; " + "an array-valued single element is also possible", + ) + ) + else: + mixed_types = any(_json_kind(item) != _json_kind(value[0]) for item in value) + if mixed_types and kind is DataElementKind.MULTI_VALUED: + raise DppJsonException( + f"{path} resolved MultiValuedDataElement requires like-typed items" + ) + if kind is None and mixed_types: + expanded = SingleValuedDataElement(element_id=element_id, value=value) + issues.append( + ExpansionIssue( + path, + "mixed array items retained as a single JSON value because a " + "multi-valued element requires like-typed items", + ) + ) + else: + value_type_missing = True + if kind is None: + issues.append( + ExpansionIssue( + path, + "MultiValuedDataElement was inferred from an array; " + "an array-valued single element is also possible", + ) + ) + old_values = ( + previous.value if isinstance(previous, MultiValuedDataElement) else () + ) + children = tuple( + self._expand_element( + str(i), + item, + f"{path}[{i}]", + semantic_path.child(str(i)), + content_specification_ids, + resolver, + old_values[i] if i < len(old_values) else None, + issues, + ) + for i, item in enumerate(value) + ) + if kind is None: + issues.append( + ExpansionIssue( + path, + "original array-item elementIds are absent; " + "positional identifiers were used", + ) + ) + expanded = MultiValuedDataElement( + element_id=definition.element_id if definition else element_id, + dictionary_reference=definition.dictionary_reference + if definition + else None, + value_data_type=definition.value_data_type if definition else None, + value=children, + ) + else: + value_type_missing = True + expanded = SingleValuedDataElement(element_id=element_id, value=value) + + if not retained_single_value: + if definition is None or not definition.dictionary_reference_resolved: + issues.append( + ExpansionIssue(path, "dictionaryReference is absent from operational JSON") + ) + if value_type_missing and ( + definition is None or not definition.value_data_type_resolved + ): + issues.append(ExpansionIssue(path, "valueDataType is absent from operational JSON")) + + if definition is not None: + expanded = self._apply_definition(expanded, definition, previous) + elif previous is not None: + expanded = self._restore_metadata(expanded, previous) + return expanded + + @staticmethod + def _looks_resource(value: Mapping[str, Any]) -> bool: + return "contentType" in value and "url" in value + + @staticmethod + def _is_language_array(value: list[Any]) -> bool: + return bool(value) and all( + isinstance(item, dict) and {"value", "language"} == set(item) for item in value + ) + + @staticmethod + def _restore_metadata(fresh: DataElement, old: DataElement) -> _DataElementModel: + if type(fresh) is not type(old): + if isinstance(old, SingleValuedDataElement): + raise DppJsonException( + "stored single-valued element was not retained during expansion" + ) + raise DppJsonException("compressed value cannot change the stored DataElement subtype") + values: dict[str, Any] = { + "element_id": old.element_id, + "dictionary_reference": old.dictionary_reference, + } + if isinstance(fresh, SingleValuedDataElement) and isinstance(old, SingleValuedDataElement): + values["value_data_type"] = old.value_data_type + if isinstance(fresh, MultiValuedDataElement) and isinstance(old, MultiValuedDataElement): + values["value_data_type"] = old.value_data_type + values["value"] = tuple( + DppJsonCodec._restore_metadata(child, old.value[i]) if i < len(old.value) else child + for i, child in enumerate(fresh.value) + ) + if isinstance(fresh, DataElementCollection) and isinstance(old, DataElementCollection): + old_by_id = {item.element_id: item for item in old.elements} + values["elements"] = tuple( + DppJsonCodec._restore_metadata(child, old_by_id[child.element_id]) + if child.element_id in old_by_id + else child + for child in fresh.elements + ) + return cast(_DataElementModel, fresh.with_updates(**values)) + + @staticmethod + def _check_definition( + element_id: str, definition: SemanticDefinition, previous: DataElement | None, path: str + ) -> None: + positional_id = element_id.isascii() and element_id.isdigit() + if definition.element_id and element_id != definition.element_id and not positional_id: + raise DppJsonException( + f"{path} semantic definition elementId conflicts with compressed elementId" + ) + if previous is not None: + if definition.kind and _kind_of(previous) is not definition.kind: + raise DppJsonException( + f"{path} semantic definition conflicts with stored DataElement kind" + ) + if definition.element_id and previous.element_id != definition.element_id: + raise DppJsonException( + f"{path} semantic definition conflicts with stored elementId" + ) + if ( + definition.dictionary_reference_resolved + and previous.dictionary_reference != definition.dictionary_reference + ): + raise DppJsonException( + f"{path} semantic definition conflicts with stored dictionaryReference" + ) + previous_type = ( + previous.value_data_type + if isinstance(previous, (SingleValuedDataElement, MultiValuedDataElement)) + else None + ) + if definition.value_data_type_resolved and previous_type != definition.value_data_type: + raise DppJsonException( + f"{path} semantic definition conflicts with stored valueDataType" + ) + + @staticmethod + def _apply_definition( + element: DataElement, + definition: SemanticDefinition, + previous: DataElement | None, + ) -> _DataElementModel: + changes: dict[str, Any] = { + "element_id": definition.element_id + or (previous.element_id if previous else element.element_id), + "dictionary_reference": ( + definition.dictionary_reference + if definition.dictionary_reference_resolved + else previous.dictionary_reference + if previous + else element.dictionary_reference + ), + } + previous_type = ( + previous.value_data_type + if isinstance(previous, (SingleValuedDataElement, MultiValuedDataElement)) + else None + ) + if isinstance(element, (SingleValuedDataElement, MultiValuedDataElement)): + changes["value_data_type"] = ( + definition.value_data_type + if definition.value_data_type_resolved + else previous_type + if previous_type is not None + else element.value_data_type + ) + if definition.value_data_type is not None and not isinstance( + element, (SingleValuedDataElement, MultiValuedDataElement) + ): + raise DppJsonException("valueDataType is not valid for the resolved DataElement kind") + return ( + cast(_DataElementModel, element.with_updates(**changes)) + if changes + else cast(_DataElementModel, element) + ) + + +def _kind_of(element: DataElement) -> DataElementKind: + if isinstance(element, DataElementCollection): + return DataElementKind.COLLECTION + if isinstance(element, SingleValuedDataElement): + return DataElementKind.SINGLE + if isinstance(element, MultiValuedDataElement): + return DataElementKind.MULTI_VALUED + if isinstance(element, MultiLanguageDataElement): + return DataElementKind.MULTI_LANGUAGE + return DataElementKind.RELATED_RESOURCE + + +def _json_kind(value: Any) -> str: + if value is None: + return "null" + if isinstance(value, bool): + return "boolean" + if isinstance(value, (int, float, Decimal)): + return "number" + if isinstance(value, str): + return "string" + if isinstance(value, list): + return "array" + if isinstance(value, dict): + return "object" + return type(value).__name__ diff --git a/src/dpp_sdk/core/model.py b/src/dpp_sdk/core/model.py index 0bc40a6..2c63090 100644 --- a/src/dpp_sdk/core/model.py +++ b/src/dpp_sdk/core/model.py @@ -2,8 +2,8 @@ Each class is a frozen Pydantic v2 model that doubles as the JSON transport payload: ``UUID`` serializes to its canonical string and ``date`` to ISO -``yyyy-MM-dd``, matching the Java ``*Payload`` POJOs exactly. Field names are the -verbatim camelCase JSON keys used by the Java SDK (Jackson applied no renaming). +``yyyy-MM-dd``, matching the Java ``*Payload`` POJOs exactly. Python attributes +use snake_case and field aliases preserve the canonical camelCase JSON keys. Only *structural* rules (the checks performed by the Java ``Builder.build()`` methods) are enforced here on construction. *Semantic* rules (e.g. "no future @@ -13,34 +13,21 @@ from __future__ import annotations -from datetime import date +import math +from collections.abc import Mapping, Sequence +from decimal import Decimal from enum import StrEnum -from typing import Annotated, Self -from uuid import UUID +from typing import Annotated, Any, Literal, NoReturn, Self -from pydantic import AfterValidator, BaseModel, ConfigDict, Field +from pydantic import ( + AwareDatetime, + BaseModel, + ConfigDict, + Field, + field_validator, +) -from ._text import is_blank - - -def _require_non_blank(value: str) -> str: - if is_blank(value): - raise ValueError("must not be blank") - return value - - -def _reject_blank_if_present(value: str | None) -> str | None: - if value is not None and is_blank(value): - raise ValueError("must not be blank if provided") - return value - - -# A required, non-blank string (Java: `x == null || x.isBlank()` guard). -NonBlankStr = Annotated[str, AfterValidator(_require_non_blank)] -# An optional string that must not be blank when present. -OptionalStr = Annotated[str | None, AfterValidator(_reject_blank_if_present)] -# A non-negative integer used for optional count fields. -NonNegativeInt = Annotated[int, Field(ge=0)] +from .semantic import SemanticPath class _Base(BaseModel): @@ -54,158 +41,285 @@ class _Base(BaseModel): def with_updates(self, **changes: object) -> Self: """Return a structurally revalidated immutable copy with ``changes``.""" - return type(self).model_validate({**self.model_dump(mode="python"), **changes}) - - -class OrganizationRole(StrEnum): - """Structural role an organization can play (port of ``OrganizationRole``).""" - - MANUFACTURER = "MANUFACTURER" - SUPPLIER = "SUPPLIER" - DISTRIBUTOR = "DISTRIBUTOR" - - -class Address(_Base): - country: NonBlankStr - zipCode: OptionalStr = None - region: OptionalStr = None - town: NonBlankStr - street: OptionalStr = None - - -class Email(_Base): - emailAddress: NonBlankStr - typeOfEmail: OptionalStr = None - - -class Telephone(_Base): - telephoneNumber: NonBlankStr - typeOfTelephone: OptionalStr = None - - -class Contact(_Base): - organization: NonBlankStr - address: Address | None = None - email: Email | None = None - telephone: Telephone | None = None - - -class Organization(_Base): - name: NonBlankStr - gln: OptionalStr = None - productDescription: OptionalStr = None - productDesignation: OptionalStr = None - productFamily: OptionalStr = None - productRoot: OptionalStr = None - productOrderSuffix: OptionalStr = None - uri: OptionalStr = None - contact: Contact | None = None - role: OrganizationRole | None = None - - -class Nameplate(_Base): - gtinCode: NonBlankStr - internalArticleNumber: OptionalStr = None - batchNumber: OptionalStr = None - customsTariffNumber: OptionalStr = None - uriOfTheProduct: OptionalStr = None - manufacturer: Organization | None = None - supplier: Organization | None = None - - -class Documentation(_Base): - digitalInstructionsLink: OptionalStr = None - safetyInstructionsLink: OptionalStr = None - downloadable: bool = False - availableForYears: NonNegativeInt | None = None - paperCopyAvailableOnRequest: bool = False - - -class PassportMetadata(_Base): - uniqueProductIdentifier: UUID - passportUpdateDates: tuple[date, ...] = Field(min_length=1) - qrCodeOrDigitalTag: OptionalStr = None - externalDocumentationLink: OptionalStr = None - - -class DppCore(_Base): - passportMetadata: PassportMetadata - nameplate: Nameplate - documentation: Documentation | None = None - - -class Dpp(_Base): - """Abstract base for DPP aggregates (port of ``Dpp``). - - Holds the reusable ``coreDpp`` and exposes read-through accessors plus the - standard ``dpp_id`` / ``product_id`` identifiers. Concrete aggregates (e.g. - :class:`dpp_sdk.dpp4fun.model.Dpp4Fun`) extend this with domain submodels. - """ - - coreDpp: DppCore - - @property - def passport_type(self) -> str: # pragma: no cover - overridden by subclasses - raise NotImplementedError - - # --- read-through accessors ------------------------------------------------- - @property - def passportMetadata(self) -> PassportMetadata: - return self.coreDpp.passportMetadata - - @property - def nameplate(self) -> Nameplate: - return self.coreDpp.nameplate - - @property - def documentation(self) -> Documentation | None: - return self.coreDpp.documentation - - @property - def uniqueProductIdentifier(self) -> UUID: - return self.coreDpp.passportMetadata.uniqueProductIdentifier - - @property - def passportUpdateDates(self) -> tuple[date, ...]: - return self.coreDpp.passportMetadata.passportUpdateDates - - @property - def qrCodeOrDigitalTag(self) -> str | None: - return self.coreDpp.passportMetadata.qrCodeOrDigitalTag - - @property - def externalDocumentationLink(self) -> str | None: - return self.coreDpp.passportMetadata.externalDocumentationLink - - @property - def gtinCode(self) -> str: - return self.coreDpp.nameplate.gtinCode - - @property - def manufacturer(self) -> Organization | None: - return self.coreDpp.nameplate.manufacturer - - @property - def supplier(self) -> Organization | None: - return self.coreDpp.nameplate.supplier - - @property - def digitalInstructionsLink(self) -> str | None: - doc = self.coreDpp.documentation - return None if doc is None else doc.digitalInstructionsLink - - @property - def safetyInstructionsLink(self) -> str | None: - doc = self.coreDpp.documentation - return None if doc is None else doc.safetyInstructionsLink - - # --- standard identifiers --------------------------------------------------- - @property - def dpp_id(self) -> str: - """Stable DPP identifier: the unique product identifier as a string.""" - return str(self.coreDpp.passportMetadata.uniqueProductIdentifier) - - @property - def product_id(self) -> str: - """Stable product identifier: the GTIN code.""" - return self.coreDpp.nameplate.gtinCode + fields = type(self).model_fields + aliases = {field.alias: name for name, field in fields.items() if field.alias} + payload = self.model_dump(mode="python", by_alias=True) + for name, value in changes.items(): + field_name = aliases.get(name, name) + if field_name not in fields: + payload[name] = value + else: + alias = fields[field_name].alias or field_name + payload[alias] = value + return type(self).model_validate(payload) + + +class Granularity(StrEnum): + """Passport granularity, with the canonical lowercase JSON values.""" + + MODEL = "model" + BATCH = "batch" + ITEM = "item" + + +class _FrozenDict(dict[str, Any]): + """A JSON object that prevents mutation after a value model is constructed.""" + + def __setitem__(self, key: str, value: Any) -> NoReturn: + raise TypeError("JSON value is immutable") + + def __delitem__(self, key: str) -> NoReturn: + raise TypeError("JSON value is immutable") + + def clear(self) -> NoReturn: + raise TypeError("JSON value is immutable") + + def pop(self, key: str, default: Any = ...) -> Any: + raise TypeError("JSON value is immutable") + + def popitem(self) -> NoReturn: + raise TypeError("JSON value is immutable") + + def setdefault(self, key: str, default: Any = None) -> Any: + raise TypeError("JSON value is immutable") + + def update(self, *args: Any, **kwargs: Any) -> NoReturn: + raise TypeError("JSON value is immutable") + + # Mypy models dict's `|=` signatures inconsistently with its `|` overloads. + def __ior__(self, other: Any) -> Self: # type: ignore[override,misc] + raise TypeError("JSON value is immutable") + + +def _freeze_json_value(value: Any, active: set[int] | None = None) -> Any: + """Copy JSON-compatible values into recursively immutable containers.""" + if value is None or isinstance(value, (str, bool, int)): + return value + if isinstance(value, float): + if not math.isfinite(value): + raise ValueError("value must be JSON-compatible") + return value + if isinstance(value, Decimal): + if not value.is_finite(): + raise ValueError("value must be JSON-compatible") + return value + + active = set() if active is None else active + if isinstance(value, Mapping): + identity = id(value) + if identity in active: + raise ValueError("value must not contain cycles") + if any(not isinstance(key, str) for key in value): + raise ValueError("value object keys must be strings") + active.add(identity) + try: + return _FrozenDict( + {key: _freeze_json_value(item, active) for key, item in value.items()} + ) + finally: + active.remove(identity) + if isinstance(value, (list, tuple)): + identity = id(value) + if identity in active: + raise ValueError("value must not contain cycles") + active.add(identity) + try: + return tuple(_freeze_json_value(item, active) for item in value) + finally: + active.remove(identity) + raise ValueError("value must be JSON-compatible") + + +class DataElement(_Base): + """Common identity fields and FULL-payload subtype selection.""" + + element_id: str | None = Field(default=None, alias="elementId") + object_type: str = Field(alias="objectType") + dictionary_reference: str | None = Field(default=None, alias="dictionaryReference") + + +class DataElementCollection(DataElement): + """Ordered nested DataElements (the FULL JSON field is ``elements``).""" + + object_type: Literal["DataElementCollection"] = Field( + default="DataElementCollection", alias="objectType" + ) + elements: tuple[_DataElementUnion, ...] = Field(default_factory=tuple) + + +class SingleValuedDataElement(DataElement): + """One JSON-compatible value and its optional declared datatype.""" + + object_type: Literal["SingleValuedDataElement"] = Field( + default="SingleValuedDataElement", alias="objectType" + ) + value: Any = None + value_data_type: str | None = Field(default=None, alias="valueDataType") + + @field_validator("value", mode="before") + @classmethod + def freeze_value(cls, value: Any) -> Any: + return _freeze_json_value(value) + + def __eq__(self, other: object) -> bool: + if type(self) is not type(other): + return NotImplemented + return ( + self.element_id == other.element_id + and self.object_type == other.object_type + and self.dictionary_reference == other.dictionary_reference + and self.value_data_type == other.value_data_type + and _json_values_equal(self.value, other.value) + ) + + def __hash__(self) -> int: + return hash( + ( + type(self), + self.element_id, + self.object_type, + self.dictionary_reference, + self.value_data_type, + _json_value_hash(self.value), + ) + ) + + +def _json_values_equal(left: Any, right: Any) -> bool: + if isinstance(left, bool) or isinstance(right, bool): + return type(left) is type(right) and left == right + if isinstance(left, (int, float, Decimal)) and isinstance(right, (int, float, Decimal)): + return Decimal(str(left)) == Decimal(str(right)) + if isinstance(left, Mapping) and isinstance(right, Mapping): + return left.keys() == right.keys() and all( + _json_values_equal(left[key], right[key]) for key in left + ) + if isinstance(left, (list, tuple)) and isinstance(right, (list, tuple)): + return len(left) == len(right) and all( + _json_values_equal(a, b) for a, b in zip(left, right, strict=True) + ) + return type(left) is type(right) and left == right + + +def _json_value_hash(value: Any) -> int: + if isinstance(value, bool): + return hash((bool, value)) + if isinstance(value, (int, float, Decimal)): + return hash(("number", Decimal(str(value)).normalize())) + if isinstance(value, Mapping): + return hash(frozenset((key, _json_value_hash(item)) for key, item in value.items())) + if isinstance(value, (list, tuple)): + return hash(tuple(_json_value_hash(item) for item in value)) + return hash((type(value), value)) + + +class MultiValuedDataElement(DataElement): + """Ordered repeated DataElements and their optional common datatype.""" + + object_type: Literal["MultiValuedDataElement"] = Field( + default="MultiValuedDataElement", alias="objectType" + ) + value_data_type: str | None = Field(default=None, alias="valueDataType") + value: tuple[_DataElementUnion, ...] = Field(default_factory=tuple) + + +class MultiLanguageValue(_Base): + """A nullable text value and language tag, as structurally modeled by Java.""" + + value: str | None = None + language: str | None = None + + +class MultiLanguageDataElement(DataElement): + """Ordered language-specific values in the FULL JSON ``value`` array.""" + + object_type: Literal["MultiLanguageDataElement"] = Field( + default="MultiLanguageDataElement", alias="objectType" + ) + value: tuple[MultiLanguageValue, ...] = Field(default_factory=tuple) + + +class RelatedResource(DataElement): + """Resource metadata associated with a data element.""" + + object_type: Literal["RelatedResource"] = Field(default="RelatedResource", alias="objectType") + content_type: str | None = Field(default=None, alias="contentType") + url: str | None = None + language: str | None = None + resource_title: str | None = Field(default=None, alias="resourceTitle") + + +_DataElementUnion = Annotated[ + DataElementCollection + | SingleValuedDataElement + | MultiValuedDataElement + | MultiLanguageDataElement + | RelatedResource, + Field(discriminator="object_type"), +] + + +for _element_model in ( + DataElementCollection, + SingleValuedDataElement, + MultiValuedDataElement, + MultiLanguageDataElement, + RelatedResource, +): + _element_model.model_rebuild(_types_namespace={"_DataElementUnion": _DataElementUnion}) + + +class DppHeader(_Base): + """Optional header metadata grouped by the SDK for the passport root.""" + + digital_product_passport_id: str | None = Field(default=None, alias="digitalProductPassportId") + unique_product_identifier: str | None = Field(default=None, alias="uniqueProductIdentifier") + granularity: Granularity | None = None + dpp_schema_version: str | None = Field(default=None, alias="dppSchemaVersion") + dpp_status: str | None = Field(default=None, alias="dppStatus") + last_updated: AwareDatetime | None = Field(default=None, alias="lastUpdated") + economic_operator_id: str | None = Field(default=None, alias="economicOperatorId") + facility_id: str | None = Field(default=None, alias="facilityId") + content_specification_ids: tuple[str, ...] = Field( + default_factory=tuple, alias="contentSpecificationIds" + ) + + +class DigitalProductPassport(_Base): + """Canonical passport aggregate with one required header and ordered elements.""" + + dpp_header: DppHeader + data_elements: tuple[_DataElementUnion, ...] = Field(default_factory=tuple, alias="elements") + + def find_data_element(self, path: Sequence[str] | SemanticPath) -> _DataElementUnion | None: + """Find an element by its ID path or numeric child indexes.""" + segments = path.segments if isinstance(path, SemanticPath) else path + if ( + isinstance(segments, str) + or not segments + or any(not segment.strip() for segment in segments) + ): + raise ValueError("path must contain non-blank segments") + + current = next( + (element for element in self.data_elements if element.element_id == segments[0]), None + ) + for segment in segments[1:]: + if isinstance(current, (DataElementCollection, MultiValuedDataElement)): + children = ( + current.elements + if isinstance(current, DataElementCollection) + else current.value + ) + if segment.isdecimal(): + index = int(segment) + current = children[index] if index < len(children) else None + else: + current = next( + (element for element in children if element.element_id == segment), None + ) + else: + current = None + if current is None: + break + return current diff --git a/src/dpp_sdk/core/semantic.py b/src/dpp_sdk/core/semantic.py new file mode 100644 index 0000000..461400f --- /dev/null +++ b/src/dpp_sdk/core/semantic.py @@ -0,0 +1,69 @@ +"""Semantic context used to restore information omitted by compressed JSON.""" + +from __future__ import annotations + +from collections.abc import Sequence +from dataclasses import dataclass +from enum import StrEnum +from typing import Protocol + + +class DataElementKind(StrEnum): + COLLECTION = "COLLECTION" + SINGLE = "SINGLE" + MULTI_VALUED = "MULTI_VALUED" + MULTI_LANGUAGE = "MULTI_LANGUAGE" + RELATED_RESOURCE = "RELATED_RESOURCE" + + +@dataclass(frozen=True) +class SemanticPath: + """Immutable element path, including numeric positions in compressed arrays.""" + + segments: tuple[str, ...] + + def __post_init__(self) -> None: + object.__setattr__(self, "segments", tuple(self.segments)) + if not self.segments or any(not part.strip() for part in self.segments): + raise ValueError("semantic path segments must not be blank") + + @classmethod + def of(cls, first: str, *remaining: str) -> SemanticPath: + return cls((first, *remaining)) + + def child(self, segment: str) -> SemanticPath: + return SemanticPath((*self.segments, segment)) + + +@dataclass(frozen=True) +class SemanticDefinition: + """Authoritative metadata resolved for one complete semantic path. + + A resolved flag may be true with a ``None`` value to state that the + semantic profile intentionally defines no metadata for that field. + """ + + kind: DataElementKind | None = None + element_id: str | None = None + dictionary_reference: str | None = None + value_data_type: str | None = None + dictionary_reference_resolved: bool = False + value_data_type_resolved: bool = False + + def __post_init__(self) -> None: + for name in ("element_id", "dictionary_reference", "value_data_type"): + value = getattr(self, name) + if value is not None and not value.strip(): + raise ValueError(f"{name} must not be blank") + if self.dictionary_reference is not None and not self.dictionary_reference_resolved: + object.__setattr__(self, "dictionary_reference_resolved", True) + if self.value_data_type is not None and not self.value_data_type_resolved: + object.__setattr__(self, "value_data_type_resolved", True) + + +class SemanticResolver(Protocol): + """Optional source of semantics omitted from a compressed document.""" + + def resolve( + self, content_specification_ids: Sequence[str], path: SemanticPath + ) -> SemanticDefinition | None: ... diff --git a/src/dpp_sdk/core/validation.py b/src/dpp_sdk/core/validation.py index 272ce83..1b8f637 100644 --- a/src/dpp_sdk/core/validation.py +++ b/src/dpp_sdk/core/validation.py @@ -1,199 +1,1369 @@ -"""Semantic validators for the core DPP model (port of ``dppsdk.core.validation``). - -Fail-fast: each validator raises :class:`~dpp_sdk.core.errors.DppValidationError` -on the FIRST rule violation, matching the Java ``Validator``/``ValidationService`` -contract. These checks are applied explicitly (not on construction), so a model -may be constructed — e.g. via JSON deserialization — and only later rejected here. - -Structural rules already enforced at construction (required/non-blank strings) are -re-checked defensively to mirror the Java validators one-to-one. -""" +"""Aggregating semantic validation for the canonical core DPP model.""" from __future__ import annotations +import base64 +import ipaddress +import re +from dataclasses import dataclass from datetime import date +from decimal import Decimal, InvalidOperation +from enum import StrEnum +from typing import Any +from urllib.parse import unquote_to_bytes, urlsplit -from ._text import is_blank -from .errors import DppValidationError from .model import ( - Address, - Contact, - Documentation, - DppCore, - Email, - Nameplate, - Organization, - OrganizationRole, - PassportMetadata, - Telephone, + DataElement, + DataElementCollection, + DigitalProductPassport, + DppHeader, + Granularity, + MultiLanguageDataElement, + MultiValuedDataElement, + RelatedResource, + SingleValuedDataElement, ) -def _has_text(value: str | None) -> bool: - return value is not None and not is_blank(value) +class ValidationSeverity(StrEnum): + ERROR = "ERROR" + WARNING = "WARNING" -def _require_not_blank(value: str | None, field_name: str) -> None: - if is_blank(value): - raise DppValidationError(f"{field_name} must not be blank") +class ValidationStatus(StrEnum): + VALID = "VALID" + INVALID = "INVALID" + UNSUPPORTED = "UNSUPPORTED" -def validate_address(address: Address | None) -> None: - if address is None: - return - _require_not_blank(address.country, "Address.country") - _require_not_blank(address.town, "Address.town") - if address.zipCode is not None: - _require_not_blank(address.zipCode, "Address.zipCode") - if address.region is not None: - _require_not_blank(address.region, "Address.region") - if address.street is not None: - _require_not_blank(address.street, "Address.street") +@dataclass(frozen=True, slots=True) +class ValidationIssue: + """One validation finding at a deterministic model path.""" + Severity = ValidationSeverity -def validate_email(email: Email | None) -> None: - if email is None: - return - _require_not_blank(email.emailAddress, "Email.emailAddress") - if "@" not in email.emailAddress: - raise DppValidationError( - "Email.emailAddress does not appear to be a valid email (missing @)" - ) - if email.typeOfEmail is not None: - _require_not_blank(email.typeOfEmail, "Email.typeOfEmail") + path: str + rule: str + message: str + severity: ValidationSeverity = ValidationSeverity.ERROR + def __post_init__(self) -> None: + if self.path is None or self.rule is None or self.message is None or self.severity is None: + raise ValueError("path, rule, message, and severity are required") + object.__setattr__(self, "rule", _canonical_rule(self.rule)) -def validate_telephone(telephone: Telephone | None) -> None: - if telephone is None: - return - _require_not_blank(telephone.telephoneNumber, "Telephone.telephoneNumber") - if telephone.typeOfTelephone is not None: - _require_not_blank(telephone.typeOfTelephone, "Telephone.typeOfTelephone") +@dataclass(frozen=True, slots=True, init=False) +class ValidationResult: + """Immutable ordered result of a complete core validation pass.""" -def validate_contact(contact: Contact | None) -> None: - if contact is None: - return - _require_not_blank(contact.organization, "Contact.organization") - if contact.address is None and contact.email is None and contact.telephone is None: - raise DppValidationError( - "Contact must provide at least one contact channel (address, email, or telephone)" + Status = ValidationStatus + _issues: tuple[ValidationIssue, ...] + _status: ValidationStatus + + def __init__(self, issues: tuple[ValidationIssue, ...] | list[ValidationIssue]) -> None: + immutable_issues = tuple(issues) + errors = tuple(i for i in immutable_issues if i.severity is ValidationSeverity.ERROR) + unsupported = { + "IDENTIFIER_METHOD_UNSUPPORTED", + "IDENTIFICATION_LINK_DI_UNSUPPORTED", + "GS1_DIGITAL_LINK_KEY_UNSUPPORTED", + "GS1_DIGITAL_LINK_QUERY_UNSUPPORTED", + } + status = ( + ValidationStatus.VALID + if not errors + else ValidationStatus.UNSUPPORTED + if all(issue.rule in unsupported for issue in errors) + else ValidationStatus.INVALID ) - validate_address(contact.address) - validate_email(contact.email) - validate_telephone(contact.telephone) - - -def validate_organization(organization: Organization | None) -> None: - if organization is None: - raise DppValidationError("Organization is required") - _require_not_blank(organization.name, "Organization.name") - if organization.uri is not None: - _require_not_blank(organization.uri, "Organization.uri") - if organization.contact is not None: - validate_contact(organization.contact) - - -def validate_nameplate(nameplate: Nameplate | None) -> None: - if nameplate is None: - raise DppValidationError("Nameplate is required") - _require_not_blank(nameplate.gtinCode, "Nameplate.gtinCode") - - for value, name in ( - (nameplate.internalArticleNumber, "Nameplate.internalArticleNumber"), - (nameplate.batchNumber, "Nameplate.batchNumber"), - (nameplate.customsTariffNumber, "Nameplate.customsTariffNumber"), - (nameplate.uriOfTheProduct, "Nameplate.uriOfTheProduct"), - ): - if value is not None: - _require_not_blank(value, name) + object.__setattr__(self, "_issues", immutable_issues) + object.__setattr__(self, "_status", status) + + @classmethod + def of(cls, issues: list[ValidationIssue] | tuple[ValidationIssue, ...]) -> ValidationResult: + return cls(issues) + + @property + def issues(self) -> tuple[ValidationIssue, ...]: + return self._issues + + @property + def status(self) -> ValidationStatus: + return self._status + + @property + def is_valid(self) -> bool: + return self.status is ValidationStatus.VALID + + +_RULE_ALIASES = { + "ELEMENT_ID_REQUIRED": "EN18223_ELEMENT_ID_REQUIRED", + "ELEMENT_ID_DUPLICATE_IN_SCOPE": "EN18223_ELEMENT_ID_UNIQUE_WITHIN_LOCATION", + "COLLECTION_EMPTY": "EN18223_COLLECTION_NON_EMPTY", + "SINGLE_VALUE_REQUIRED": "EN18223_SINGLE_VALUE_REQUIRED", + "MULTI_VALUE_EMPTY": "EN18223_MULTI_NON_EMPTY", + "MULTI_VALUE_MIXED_ELEMENT_TYPES": "EN18223_MULTI_HOMOGENEOUS_KIND", + "MULTI_VALUE_JSON_TYPE_MISMATCH": "EN18223_JSON_NATIVE_TYPE_MATCHES_XSD", + "MULTI_LANGUAGE_EMPTY": "EN18223_MULTILANGUAGE_NON_EMPTY", + "MULTI_LANGUAGE_VALUE_REQUIRED": "EN18223_MULTILANGUAGE_VALUE_REQUIRED", + "MULTI_LANGUAGE_LANGUAGE_REQUIRED": "EN18223_MULTILANGUAGE_LANGUAGE_REQUIRED", + "RELATED_RESOURCE_CONTENT_TYPE_REQUIRED": "EN18223_RELATED_RESOURCE_CONTENT_TYPE_REQUIRED", + "RELATED_RESOURCE_URL_REQUIRED": "EN18223_RELATED_RESOURCE_URL_REQUIRED", + "RESOURCE_URL_INVALID": "EN18223_RELATED_RESOURCE_URL_RFC3986", + "MEDIA_TYPE_INVALID": "EN18223_RELATED_RESOURCE_CONTENT_TYPE_SYNTAX", + "LOCALIZED_LANGUAGE_FORMAT_INVALID": "EN18223_MULTILANGUAGE_LANGUAGE_TAG", + "LANGUAGE_CODE_UNKNOWN": "EN18223_MULTILANGUAGE_LANGUAGE_TAG", + "COUNTRY_CODE_UNKNOWN": "EN18223_MULTILANGUAGE_LANGUAGE_TAG", + "VALUE_DATA_TYPE_PROHIBITED": "EN18223_VALUE_DATATYPE_SUPPORTED", + "VALUE_DATA_TYPE_UNKNOWN": "EN18223_VALUE_DATATYPE_SUPPORTED", + "VALUE_TYPE_INCOMPATIBLE": "EN18223_VALUE_MATCHES_DATATYPE", + "VALUE_FORMAT_INVALID": "EN18223_VALUE_MATCHES_DATATYPE", + "MULTI_VALUE_DATA_TYPE_CONFLICT": "EN18223_MULTI_HOMOGENEOUS_DATATYPE", + "MULTI_VALUE_DATA_TYPE_MISMATCH": "EN18223_MULTI_HOMOGENEOUS_DATATYPE", +} + - if nameplate.manufacturer is None and nameplate.supplier is None: - raise DppValidationError("Nameplate must have at least a manufacturer or supplier") +def _canonical_rule(rule: str) -> str: + return _RULE_ALIASES.get(rule, rule) - if nameplate.manufacturer is not None: - validate_organization(nameplate.manufacturer) - if nameplate.manufacturer.role is None: - raise DppValidationError( - "Nameplate.manufacturer must have role MANUFACTURER, but role is null" + +class DppValidator: + """Checks mandatory EN 18223 Clause 4 semantics without raising on findings.""" + + def validate(self, passport: DigitalProductPassport | None) -> ValidationResult: + issues: list[ValidationIssue] = [] + if passport is None: + return ValidationResult( + [ValidationIssue("dpp", "DPP_REQUIRED", "DigitalProductPassport is required")] ) - if nameplate.manufacturer.role is not OrganizationRole.MANUFACTURER: - raise DppValidationError( - "Nameplate.manufacturer must have role MANUFACTURER, but got " - + nameplate.manufacturer.role.value + header = passport.dpp_header + for name, value, rule in ( + ( + "digitalProductPassportId", + header.digital_product_passport_id, + "EN18223_HEADER_DPP_ID_REQUIRED", + ), + ( + "uniqueProductIdentifier", + header.unique_product_identifier, + "EN18223_HEADER_PRODUCT_ID_REQUIRED", + ), + ("granularity", header.granularity, "EN18223_HEADER_GRANULARITY_REQUIRED"), + ( + "dppSchemaVersion", + header.dpp_schema_version, + "EN18223_HEADER_SCHEMA_VERSION_REQUIRED", + ), + ("dppStatus", header.dpp_status, "EN18223_HEADER_STATUS_REQUIRED"), + ("lastUpdated", header.last_updated, "EN18223_HEADER_LAST_UPDATED_REQUIRED"), + ( + "economicOperatorId", + header.economic_operator_id, + "EN18223_HEADER_ECONOMIC_OPERATOR_ID_REQUIRED", + ), + ): + if value is None: + issues.append(ValidationIssue(f"header.{name}", rule, f"{name} is required")) + dpp_id = header.digital_product_passport_id + if dpp_id is not None and not _absolute_uri(dpp_id): + issues.append( + ValidationIssue( + "header.digitalProductPassportId", + "EN18223_HEADER_DPP_ID_URI_RECOMMENDED", + "digitalProductPassportId should be an absolute URI", + ValidationSeverity.WARNING, + ) ) + for value in header.content_specification_ids: + if value is None or not value.strip(): + issues.append( + ValidationIssue( + "header.contentSpecificationIds", + "EN18223_HEADER_CONTENT_SPEC_ID_NONBLANK", + "contentSpecificationId must not be blank", + ) + ) + + seen_paths: set[str] = set() + _absolute_paths(passport.data_elements, "elements", "elements", seen_paths, issues) + _validate_elements(passport.data_elements, "elements", issues) + return ValidationResult(issues) + + +def _absolute_uri(value: str) -> bool: + return _uri_valid(value) + - if nameplate.supplier is not None: - validate_organization(nameplate.supplier) - if nameplate.supplier.role is None: - raise DppValidationError("Nameplate.supplier must have role SUPPLIER, but role is null") - if nameplate.supplier.role is not OrganizationRole.SUPPLIER: - raise DppValidationError( - "Nameplate.supplier must have role SUPPLIER, but got " - + nameplate.supplier.role.value +def _children(element: DataElement) -> tuple[DataElement, ...]: + if isinstance(element, DataElementCollection): + return element.elements + if isinstance(element, MultiValuedDataElement): + return element.value + return () + + +def _absolute_paths( + elements: tuple[DataElement, ...], + semantic_parent: str, + display_parent: str, + seen: set[str], + issues: list[ValidationIssue], +) -> None: + for index, element in enumerate(elements): + element_id = element.element_id + if element_id is None or not element_id.strip(): + continue + absolute = f"{semantic_parent}.{element_id}" + if absolute in seen: + issues.append( + ValidationIssue( + f"{display_parent}[{index}].elementId", + "EN18223_ABSOLUTE_PATH_UNIQUE", + "absolute DataElement paths must be unique", + ) ) + seen.add(absolute) + children = _children(element) + if children: + _absolute_paths(children, absolute, f"{display_parent}[{index}].elements", seen, issues) -def validate_documentation(documentation: Documentation | None) -> None: - if documentation is None: - return +def _json_type(element: DataElement) -> str | None: + if isinstance(element, SingleValuedDataElement): + value = element.value + if value is None: + return None + if isinstance(value, bool): + return "boolean" + if isinstance(value, (int, float, Decimal)): + return "number" + if isinstance(value, str): + return "string" + if isinstance(value, dict): + return "object" + if isinstance(value, (list, tuple)): + return "array" + if isinstance(element, (DataElementCollection, RelatedResource)): + return "object" + if isinstance(element, (MultiValuedDataElement, MultiLanguageDataElement)): + return "array" + return None - digital_link = documentation.digitalInstructionsLink - safety_link = documentation.safetyInstructionsLink - if digital_link is not None: - _require_not_blank(digital_link, "Documentation.digitalInstructionsLink") - if safety_link is not None: - _require_not_blank(safety_link, "Documentation.safetyInstructionsLink") +def _validate_elements( + elements: tuple[DataElement, ...], path: str, issues: list[ValidationIssue] +) -> None: + ids: set[str] = set() + for index, element in enumerate(elements): + element_path = f"{path}[{index}]" + element_id = element.element_id + if element_id is None or not element_id.strip(): + issues.append( + ValidationIssue( + f"{element_path}.elementId", + "EN18223_ELEMENT_ID_REQUIRED", + "elementId is required", + ) + ) + elif element_id in ids: + issues.append( + ValidationIssue( + f"{element_path}.elementId", + "EN18223_ELEMENT_ID_UNIQUE_WITHIN_LOCATION", + "elementId must be unique among elements in the same location", + ) + ) + else: + ids.add(element_id) - has_any_link = _has_text(digital_link) or _has_text(safety_link) + if isinstance(element, DataElementCollection): + child_path = f"{element_path}.elements" + if not element.elements: + issues.append( + ValidationIssue( + child_path, + "EN18223_COLLECTION_NON_EMPTY", + "DataElementCollection must contain at least one DataElement", + ) + ) + else: + _validate_elements(element.elements, child_path, issues) + elif isinstance(element, SingleValuedDataElement): + if element.value is None: + issues.append( + ValidationIssue( + f"{element_path}.value", + "EN18223_SINGLE_VALUE_REQUIRED", + "SingleValuedDataElement value is required", + ) + ) + elif isinstance(element, MultiValuedDataElement): + child_path = f"{element_path}.elements" + if not element.value: + issues.append( + ValidationIssue( + child_path, + "EN18223_MULTI_NON_EMPTY", + "MultiValuedDataElement must contain at least one DataElement", + ) + ) + else: + expected_class = type(element.value[0]) + mixed = False + for child_index, child in enumerate(element.value[1:], 1): + if type(child) is not expected_class: + mixed = True + issues.append( + ValidationIssue( + f"{child_path}[{child_index}]", + "EN18223_MULTI_HOMOGENEOUS_KIND", + "All DataElements in a MultiValuedDataElement must have " + "the same concrete type", + ) + ) + if not mixed: + expected_json_type: str | None = None + for child_index, child in enumerate(element.value): + current_type = _json_type(child) + if current_type is None: + continue + if expected_json_type is None: + expected_json_type = current_type + elif current_type != expected_json_type: + issues.append( + ValidationIssue( + f"{child_path}[{child_index}]", + "MULTI_VALUE_JSON_TYPE_MISMATCH", + "All values in a MultiValuedDataElement must serialize " + "to the same JSON type", + ) + ) + _validate_elements(element.value, child_path, issues) + elif isinstance(element, RelatedResource): + if element.content_type is None: + issues.append( + ValidationIssue( + f"{element_path}.contentType", + "RELATED_RESOURCE_CONTENT_TYPE_REQUIRED", + "contentType is required", + ) + ) + if element.url is None: + issues.append( + ValidationIssue( + f"{element_path}.url", "RELATED_RESOURCE_URL_REQUIRED", "url is required" + ) + ) + elif isinstance(element, MultiLanguageDataElement): + values_path = f"{element_path}.values" + if not element.value: + issues.append( + ValidationIssue( + values_path, + "MULTI_LANGUAGE_EMPTY", + "MultiLanguageDataElement must contain at least one MultiLanguageValue", + ) + ) + for value_index, value in enumerate(element.value): + value_path = f"{values_path}[{value_index}]" + if value.value is None: + issues.append( + ValidationIssue( + f"{value_path}.value", + "MULTI_LANGUAGE_VALUE_REQUIRED", + "value is required", + ) + ) + if value.language is None: + issues.append( + ValidationIssue( + f"{value_path}.language", + "MULTI_LANGUAGE_LANGUAGE_REQUIRED", + "language is required", + ) + ) + else: + issues.append( + ValidationIssue( + element_path, + "EN18223_OBJECT_TYPE_MATCHES_RUNTIME_KIND", + f"Unsupported DataElement implementation: {type(element).__name__}", + ) + ) - if documentation.downloadable and not has_any_link: - raise DppValidationError( - "Documentation is marked as downloadable but no documentation links are provided" + +_XSD_ALLOWED = frozenset( # noqa: SIM905 - compact source-backed type table + """ +anySimpleType string boolean decimal float double duration dateTime time date gYearMonth gYear +gMonthDay gDay gMonth hexBinary base64Binary anyURI integer nonPositiveInteger negativeInteger +long int short byte nonNegativeInteger unsignedLong unsignedInt unsignedShort unsignedByte +positiveInteger +""".split() # noqa: SIM905 +) +_XSD_PROHIBITED = frozenset( # noqa: SIM905 - compact source-backed type table + """ +ENTITIES IDREFS NMTOKENS normalizedString token language NMTOKEN Name NCName ID IDREF ENTITY +NOTATION QName +""".split() # noqa: SIM905 +) +_LANGUAGE_CODES = frozenset( # noqa: SIM905 - compact pinned ISO code table + """ +aa ab ae af ak am an ar as av ay az ba be bg bh bi bm bn bo br bs ca ce ch co cr cs cu cv cy +da de dv dz ee el en eo es et eu fa ff fi fj fo fr fy ga gd gl gn gu gv ha he hi ho hr ht hu hy +hz ia id ie ig ii ik io is it iu ja jv ka kg ki kj kk kl km kn ko kr ks ku kv kw ky la lb lg li ln +lo lt lu lv mg mh mi mk ml mn mr ms mt my na nb nd ne ng nl nn no nr nv ny oc oj om or os pa pi +pl ps pt qu rm rn ro ru rw sa sc sd se sg si sk sl sm sn so sq sr ss st su sv sw ta te tg th ti tk +tl tn to tr ts tt tw ty ug uk ur uz ve vi vo wa wo xh yi yo za zh zu +""".split() # noqa: SIM905 +) +_COUNTRY_CODES = frozenset( # noqa: SIM905 - compact pinned ISO code table + """ +AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS +BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE +EG EH ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HM +HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY KZ LA LB LC +LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA +NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY QA RE RO RS RU RW +SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF TG TH TJ TK TL TM TN TO +TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS YE YT ZA ZM ZW +""".split() # noqa: SIM905 +) + + +def _datatype(value: str) -> str: + if not value.startswith("xsd:") or len(value) == 4: + return "unknown" + name = value[4:] + if name in _XSD_PROHIBITED: + return "prohibited" + return "allowed" if name in _XSD_ALLOWED else "unknown" + + +def _media_type_valid(value: str) -> bool: + token = r"[A-Za-z0-9!#$%&'*+.^_`|~-]+" + match = re.fullmatch(rf"({token})/({token})(.*)", value) + if match is None or len(match[1]) > 127 or len(match[2]) > 127: + return False + suffix = match[3] + names: set[str] = set() + while suffix: + parameter = re.match( + rf";[ \t]*({token})=({token}|\"(?:[^\"\\\r\n]|\\[^\r\n])*\")[ \t]*", suffix ) + if parameter is None: + return False + name = parameter[1].lower() + if name in names: + return False + names.add(name) + suffix = suffix[parameter.end() :] + return True + + +def _uri_valid(value: str) -> bool: + if not value or any(ord(char) < 0x21 or ord(char) > 0x7E for char in value): + return False + scheme = re.match(r"^([A-Za-z][A-Za-z0-9+.-]*):", value) + if scheme is None: + return False + remainder = value[scheme.end() :] + if remainder.count("#") > 1: + return False + hierarchy, hash_mark, fragment = remainder.partition("#") + before_query, query_mark, query = hierarchy.partition("?") + if hash_mark and not _uri_chars_valid(fragment, "/?", allow_colon=True): + return False + if query_mark and not _uri_chars_valid(query, "/?", allow_colon=True): + return False + if before_query.startswith("//"): + authority_path = before_query[2:] + authority, slash, path = authority_path.partition("/") + if not _authority_valid(authority): + return False + return not slash or _uri_chars_valid(path, "/", allow_colon=True) + return _uri_chars_valid(before_query, "/", allow_colon=True) - if documentation.availableForYears is not None: - if documentation.availableForYears < 0: - raise DppValidationError( - "Documentation.availableForYears must be non-negative, but got " - + str(documentation.availableForYears) + +def _uri_chars_valid(value: str, extra: str, *, allow_colon: bool) -> bool: + allowed = ( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~!$&'()*+,;=" + extra + ) + if "/" in extra: + allowed += "@" + if allow_colon: + allowed += ":" + index = 0 + while index < len(value): + char = value[index] + if char == "%": + if re.fullmatch(r"[0-9A-Fa-f]{2}", value[index + 1 : index + 3]) is None: + return False + index += 3 + elif char in allowed: + index += 1 + else: + return False + return True + + +def _authority_valid(authority: str) -> bool: + if authority.count("@") > 1: + return False + userinfo, at, server = authority.rpartition("@") + if at and not _uri_chars_valid(userinfo, ":", allow_colon=True): + return False + if server.startswith("["): + end = server.find("]") + if end < 0: + return False + literal = server[1:end] + if literal.lower().startswith("v"): + if re.fullmatch(r"[vV][0-9A-Fa-f]+\.[A-Za-z0-9._~!$&'()*+,;=:-]+", literal) is None: + return False + else: + try: + ipaddress.IPv6Address(literal) + except ipaddress.AddressValueError: + return False + suffix = server[end + 1 :] + return not (suffix and (not suffix.startswith(":") or not suffix[1:].isdigit())) + if server.count(":") > 1: + return False + host, colon, port = server.partition(":") + if colon and not all(char.isascii() and char.isdigit() for char in port): + return False + return _uri_chars_valid(host, ".", allow_colon=False) + + +def _language_valid(value: str) -> bool: + if re.fullmatch(r"[A-Za-z]{2}-[A-Za-z]{2}", value) is None: + return False + return value[:2].lower() in _LANGUAGE_CODES and value[3:].upper() in _COUNTRY_CODES + + +def _value_failure(datatype: str, value: Any) -> tuple[str, str] | None: + name = datatype[4:] + if name in ("string", "anySimpleType"): + if name == "anySimpleType": + return None + good = isinstance(value, str) + elif name == "boolean": + good = isinstance(value, bool) + elif name in ("float", "double"): + good = isinstance(value, (int, float, Decimal)) and not isinstance(value, bool) + elif name == "decimal": + good = isinstance(value, (int, float, Decimal)) and not isinstance(value, bool) + if isinstance(value, str): + normalized = re.sub(r"[ \t\r\n]+", " ", value).strip() + if re.fullmatch(r"[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)", normalized) is None: + return "VALUE_FORMAT_INVALID", "value is not in the required decimal string format" + try: + Decimal(normalized) + good = True + except InvalidOperation: + return "VALUE_FORMAT_INVALID", "value is not in the required decimal string format" + if not good: + return "VALUE_TYPE_INCOMPATIBLE", "value must be a JSON number or decimal string" + elif name in ( + "integer", + "long", + "int", + "short", + "byte", + "positiveInteger", + "nonNegativeInteger", + "negativeInteger", + "nonPositiveInteger", + "unsignedLong", + "unsignedInt", + "unsignedShort", + "unsignedByte", + ): + if ( + not isinstance(value, (int, float, Decimal)) + or isinstance(value, bool) + or int(value) != value + ): + return "VALUE_TYPE_INCOMPATIBLE", "value must be a JSON integral number" + number = int(value) + bounds = { + "positiveInteger": (1, None), + "nonNegativeInteger": (0, None), + "negativeInteger": (None, -1), + "nonPositiveInteger": (None, 0), + "long": (-(2**63), 2**63 - 1), + "int": (-(2**31), 2**31 - 1), + "short": (-(2**15), 2**15 - 1), + "byte": (-(2**7), 2**7 - 1), + "unsignedLong": (0, 2**64 - 1), + "unsignedInt": (0, 2**32 - 1), + "unsignedShort": (0, 65535), + "unsignedByte": (0, 255), + } + lower, upper = bounds.get(name, (None, None)) + if lower is not None and number < lower or upper is not None and number > upper: + return "VALUE_FORMAT_INVALID", f"value is outside the {name} integer range" + return None + elif name == "date": + good = isinstance(value, str) and re.fullmatch(r"\d{4}-\d{2}-\d{2}", value) is not None + if good: + try: + date.fromisoformat(value) + except ValueError: + return ( + "VALUE_FORMAT_INVALID", + "value is not in the required real calendar date format", + ) + elif isinstance(value, str): + return "VALUE_FORMAT_INVALID", "value is not in the required YYYY-MM-DD date format" + else: + return "VALUE_TYPE_INCOMPATIBLE", "value must be a JSON date string" + elif name == "dateTime": + good = ( + isinstance(value, str) + and re.fullmatch(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z", value) is not None + ) + if good: + try: + date.fromisoformat(value[:10]) + hour, minute, second = ( + int(value[start:end]) for start, end in ((11, 13), (14, 16), (17, 19)) + ) + if hour == 24 and minute == 0 and second == 0: + return None + if hour > 23 or minute > 59: + raise ValueError + if second <= 59: + return None + leap_second_dates = { + "1972-06-30", + "1972-12-31", + "1973-12-31", + "1974-12-31", + "1975-12-31", + "1976-12-31", + "1977-12-31", + "1978-12-31", + "1979-12-31", + "1981-06-30", + "1982-06-30", + "1983-06-30", + "1985-06-30", + "1987-12-31", + "1989-12-31", + "1990-12-31", + "1992-06-30", + "1993-06-30", + "1994-06-30", + "1995-12-31", + "1997-06-30", + "1998-12-31", + "2005-12-31", + "2008-12-31", + "2012-06-30", + "2015-06-30", + "2016-12-31", + } + if second == 60 and hour == 23 and minute == 59 and value[:10] in leap_second_dates: + return None + raise ValueError + except ValueError: + return ( + "VALUE_FORMAT_INVALID", + "value is not in the required real UTC dateTime format", + ) + elif isinstance(value, str): + return ( + "VALUE_FORMAT_INVALID", + "value is not in the required YYYY-MM-DDThh:mm:ssZ dateTime format", ) - if not has_any_link: - raise DppValidationError( - "Documentation.availableForYears is set but no documentation links are provided" + else: + return "VALUE_TYPE_INCOMPATIBLE", "value must be a UTC dateTime string" + elif name == "anyURI": + if not isinstance(value, str): + return "VALUE_TYPE_INCOMPATIBLE", "value must be a URI string" + if not _uri_valid(value): + return "VALUE_FORMAT_INVALID", "value is not in the required RFC 3986 URI format" + return None + elif name == "base64Binary": + if not isinstance(value, str): + return "VALUE_TYPE_INCOMPATIBLE", "value must be a base64 string" + collapsed = re.sub(r"[ \t\r\n]+", " ", value).strip().replace(" ", "") + try: + if ( + len(collapsed) % 4 + or base64.b64encode(base64.b64decode(collapsed, validate=True)).decode() + != collapsed + ): + raise ValueError + except ValueError: + return "VALUE_FORMAT_INVALID", "value is not in the required base64 encoding format" + return None + else: + return None + return ( + None + if good + else ("VALUE_TYPE_INCOMPATIBLE", "value has a JSON type incompatible with its datatype") + ) + + +class DataFormatValidator: + """Checks locally verifiable JSON, datatype, URI, media, and language formats.""" + + def validate(self, passport: DigitalProductPassport | None) -> ValidationResult: + issues: list[ValidationIssue] = [] + if passport is not None: + self._elements(passport.data_elements, "elements", issues) + return ValidationResult(issues) + + def _elements( + self, elements: tuple[DataElement, ...], path: str, issues: list[ValidationIssue] + ) -> None: + for index, element in enumerate(elements): + self._element(element, f"{path}[{index}]", issues) + + def _element(self, element: DataElement, path: str, issues: list[ValidationIssue]) -> None: + if isinstance(element, RelatedResource): + if element.url is not None and not _uri_valid(element.url): + issues.append( + ValidationIssue( + f"{path}.url", + "RESOURCE_URL_INVALID", + "url must be an absolute RFC 3986 URI", + ) + ) + if element.content_type is not None and not _media_type_valid(element.content_type): + issues.append( + ValidationIssue( + f"{path}.contentType", + "MEDIA_TYPE_INVALID", + "contentType must be a syntactically valid media type", + ) + ) + if element.language is not None and not _language_valid(element.language): + issues.append( + ValidationIssue( + f"{path}.language", + "EN18223_RELATED_RESOURCE_LANGUAGE_TAG", + "language must be a recognized two-letter language and country tag", + ) + ) + elif isinstance(element, SingleValuedDataElement): + self._single(element, path, element.value_data_type, False, issues) + elif isinstance(element, DataElementCollection): + self._elements(element.elements, f"{path}.elements", issues) + elif isinstance(element, MultiValuedDataElement): + self._multi(element, path, issues) + elif isinstance(element, MultiLanguageDataElement): + for index, item in enumerate(element.value): + if item.language is not None and not _language_valid(item.language): + issues.append( + ValidationIssue( + f"{path}.values[{index}].language", + "EN18223_MULTILANGUAGE_LANGUAGE_TAG", + "language must be a recognized two-letter language and country tag", + ) + ) + + def _multi( + self, multi: MultiValuedDataElement, path: str, issues: list[ValidationIssue] + ) -> None: + container_type = multi.value_data_type + if container_type is not None: + self._check_datatype(container_type, f"{path}.valueDataType", issues) + inferred: str | None = None + for index, child in enumerate(multi.value): + child_path = f"{path}.elements[{index}]" + if isinstance(child, SingleValuedDataElement): + child_type = child.value_data_type + if ( + container_type is not None + and child_type is not None + and container_type != child_type + ): + issues.append( + ValidationIssue( + f"{child_path}.valueDataType", + "MULTI_VALUE_DATA_TYPE_CONFLICT", + "child valueDataType conflicts with its MultiValuedDataElement " + "declaration", + ) + ) + if container_type is None and child_type is not None: + if inferred is None: + inferred = child_type + elif inferred != child_type: + issues.append( + ValidationIssue( + f"{child_path}.valueDataType", + "MULTI_VALUE_DATA_TYPE_MISMATCH", + "all declared child valueDataType values in a " + "MultiValuedDataElement must match", + ) + ) + if child_type is not None: + self._single(child, child_path, child_type, False, issues) + elif container_type is not None: + self._single(child, child_path, container_type, True, issues) + else: + self._element(child, child_path, issues) + + def _check_datatype(self, datatype: str, path: str, issues: list[ValidationIssue]) -> str: + classification = _datatype(datatype) + if classification == "prohibited": + issues.append( + ValidationIssue( + path, + "VALUE_DATA_TYPE_PROHIBITED", + "valueDataType uses an XSD built-in prohibited by EN 18223", + ) + ) + elif classification == "unknown": + issues.append( + ValidationIssue( + path, + "VALUE_DATA_TYPE_UNKNOWN", + "valueDataType must use a supported xsd: spelling", + ) ) + return classification + + def _single( + self, + single: SingleValuedDataElement, + path: str, + datatype: str | None, + inherited: bool, + issues: list[ValidationIssue], + ) -> None: + if datatype is None: + return + classification = _datatype(datatype) + if not inherited: + classification = self._check_datatype(datatype, f"{path}.valueDataType", issues) + if classification != "allowed" or single.value is None: + return + failure = _value_failure(datatype, single.value) + if failure: + rule, message = failure + issues.append(ValidationIssue(f"{path}.value", rule, message)) + + +class ProductIdentifierMethod(StrEnum): + GS1_DIGITAL_LINK = "GS1_DIGITAL_LINK" + IDENTIFICATION_LINK = "IDENTIFICATION_LINK" + UNSUPPORTED = "UNSUPPORTED" + + +class EconomicOperatorIdentifierMethod(StrEnum): + GS1_GLN = "GS1_GLN" + UNSUPPORTED = "UNSUPPORTED" + + +class FacilityIdentifierMethod(StrEnum): + GS1_GLN = "GS1_GLN" + UNSUPPORTED = "UNSUPPORTED" + + +@dataclass(frozen=True, slots=True) +class IdentifierValidationContext: + """Caller-selected local identifier profiles; methods are never inferred.""" + + product_identifier_method: ProductIdentifierMethod + economic_operator_identifier_method: EconomicOperatorIdentifierMethod + facility_identifier_method: FacilityIdentifierMethod | None = None + + def __post_init__(self) -> None: + if ( + self.product_identifier_method is None + or self.economic_operator_identifier_method is None + ): + raise ValueError("product and economic-operator identifier methods are required") + + +def _gs1_check_digit(value: str, length: int) -> bool: + if len(value) != length or not value.isascii() or not value.isdigit(): + return False + total = sum( + int(char) * (3 if (length - 2 - index) % 2 == 0 else 1) + for index, char in enumerate(value[:-1]) + ) + return (10 - total % 10) % 10 == int(value[-1]) + + +def _identifier_url(value: str, schemes: tuple[str, ...]) -> tuple[str, str, str | None] | None: + if any(ord(char) < 0x21 or ord(char) > 0x7E for char in value) or re.search( + r"%(?![0-9A-Fa-f]{2})", value + ): + return None + try: + parsed = urlsplit(value) + if ( + parsed.scheme.lower() not in schemes + or not parsed.hostname + or not parsed.path + or parsed.username is not None + or parsed.password is not None + or parsed.fragment + ): + return None + _ = parsed.port + query = parsed.query if "?" in value.split("#", 1)[0] else None + return parsed.scheme, parsed.path, query + except ValueError: + return None -def validate_passport_metadata(metadata: PassportMetadata | None) -> None: - if metadata is None: - raise DppValidationError("PassportMetadata is required") - if metadata.uniqueProductIdentifier is None: - raise DppValidationError("PassportMetadata.uniqueProductIdentifier is required") - if not metadata.passportUpdateDates: - raise DppValidationError("PassportMetadata.passportUpdateDates must not be empty") +def _valid_gs1_query(query: str) -> bool: + for parameter in query.split("&"): + if "=" not in parameter: + return False + raw_key, raw_value = parameter.split("=", 1) + try: + key = unquote_to_bytes(raw_key).decode("utf-8", errors="strict") + value = unquote_to_bytes(raw_value).decode("utf-8", errors="strict") + except UnicodeError: + return False + if not key: + return False + if "=" in key: + return False + if key.isascii() and key.isdigit() and (not 2 <= len(key) <= 4 or not value): + return False + return True - today = date.today() - for index, update_date in enumerate(metadata.passportUpdateDates): - if update_date is None: - raise DppValidationError(f"PassportMetadata.passportUpdateDates[{index}] is null") - if update_date > today: - raise DppValidationError( - f"PassportMetadata.passportUpdateDates[{index}] is in the future: {update_date}" + +def _xchar_value(raw: str, maximum: int) -> bool: + unreserved = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._" + symbols = "!\"%&'()*+,-./:;<=>?ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz0123456789" + index = 0 + while index < len(raw): + char = raw[index] + if char == "%": + if ( + index + 2 >= len(raw) + or re.fullmatch(r"[0-9A-Fa-f]{2}", raw[index + 1 : index + 3]) is None + ): + return False + decoded = chr(int(raw[index + 1 : index + 3], 16)) + if decoded in unreserved or decoded not in symbols: + return False + index += 3 + elif char in unreserved: + index += 1 + else: + return False + try: + decoded_value = unquote_to_bytes(raw).decode("utf-8", errors="strict") + except UnicodeError: + return False + return ( + bool(decoded_value) + and len(decoded_value) <= maximum + and all(char in symbols for char in decoded_value) + ) + + +def _granularity_issue( + represented: Granularity, actual: Granularity | None +) -> ValidationIssue | None: + if actual is None: + return ValidationIssue( + "header.granularity", + "IDENTIFIER_GRANULARITY_REQUIRED", + "Granularity is required to compare with the selected identifier", + ) + if represented is not actual: + return ValidationIssue( + "header.granularity", + "IDENTIFIER_GRANULARITY_MISMATCH", + "Granularity does not match the product level represented by the selected identifier", + ) + return None + + +def _gs1_product(identifier: str | None, granularity: Granularity | None) -> list[ValidationIssue]: + path = "header.uniqueProductIdentifier" + parsed = None if identifier is None else _identifier_url(identifier, ("https",)) + if parsed is None or parsed[2] is not None and not _valid_gs1_query(parsed[2]): + return [ + ValidationIssue( + path, + "GS1_DIGITAL_LINK_URL_INVALID", + "GS1 Digital Link must be an absolute HTTPS URI with valid RFC 3986 encoding", + ) + ] + _, raw_path, query = parsed + segments = raw_path.split("/") + key_index = next( + ( + i + for i in range(len(segments) - 2, 0, -1) + if segments[i] == "01" and _gs1_check_digit(segments[i + 1], 14) + ), + -1, + ) + if key_index < 0: + if any( + segments[i] in {"8006", "8010", "8013"} and segments[i + 1] + for i in range(1, len(segments) - 1) + ): + return [ + ValidationIssue( + path, + "GS1_DIGITAL_LINK_KEY_UNSUPPORTED", + "This GS1 Digital Link product key is not locally validated", + ) + ] + return [ + ValidationIssue( + path, "GS1_DIGITAL_LINK_KEY_INVALID", "A GTIN primary key using AI 01 is required" ) + ] + seen: set[str] = set() + ranks = {"22": 0, "10": 1, "21": 2, "235": 3} + qualifiers: list[str] = [] + previous = -1 + for i in range(key_index + 2, len(segments), 2): + if i + 1 >= len(segments) or not segments[i] or not segments[i + 1]: + return [ + ValidationIssue( + path, + "GS1_DIGITAL_LINK_QUALIFIER_INVALID", + "GS1 Digital Link qualifiers must be complete AI/value pairs", + ) + ] + ai, raw = segments[i], segments[i + 1] + rank = ranks.get(ai, -1) + if ( + rank < 0 + or rank <= previous + or ai in seen + or ai == "235" + and qualifiers + or not _xchar_value(raw, 28 if ai == "235" else 20) + ): + return [ + ValidationIssue( + path, + "GS1_DIGITAL_LINK_QUALIFIER_INVALID", + "The GTIN key qualifiers must use supported GS1 syntax and order", + ) + ] + previous = rank + seen.add(ai) + qualifiers.append(ai) + represented = ( + Granularity.ITEM + if "21" in qualifiers or "235" in qualifiers + else Granularity.BATCH + if "10" in qualifiers + else Granularity.MODEL + ) + issue = _granularity_issue(represented, granularity) + issues = [issue] if issue else [] + if query is not None and any( + key.isdigit() for key in (piece.split("=", 1)[0] for piece in query.split("&")) + ): + issues.append( + ValidationIssue( + path, + "GS1_DIGITAL_LINK_QUERY_UNSUPPORTED", + "GS1 numeric query attributes require AI/value validation not implemented by " + "this local profile", + ) + ) + return issues - if metadata.qrCodeOrDigitalTag is not None: - _require_not_blank(metadata.qrCodeOrDigitalTag, "PassportMetadata.qrCodeOrDigitalTag") - if metadata.externalDocumentationLink is not None: - _require_not_blank( - metadata.externalDocumentationLink, "PassportMetadata.externalDocumentationLink" + +def _gln(identifier: str | None, path: str, role: str) -> list[ValidationIssue]: + if not identifier: + return [ValidationIssue(path, "GS1_GLN_INVALID", "A GS1 GLN is required")] + ai: str | None = None + query_issue: ValidationIssue | None = None + key = identifier + if identifier.lower().startswith(("https://", "http://")): + parsed = _identifier_url(identifier, ("http", "https")) + if parsed is None: + return [ + ValidationIssue( + path, + "GS1_GLN_INVALID", + "A GS1 GLN Digital Link must be a valid HTTP or HTTPS URI", + ) + ] + _, raw_path, query = parsed + if query is not None and not _valid_gs1_query(query): + return [ + ValidationIssue( + path, "GS1_GLN_INVALID", "A GS1 GLN Digital Link must use valid query syntax" + ) + ] + if query is not None and any( + piece.split("=", 1)[0].isdigit() for piece in query.split("&") + ): + query_issue = ValidationIssue( + path, + "GS1_DIGITAL_LINK_QUERY_UNSUPPORTED", + "GS1 numeric query attributes require AI/value validation not implemented by " + "this local profile", + ) + segments = raw_path.split("/") + ix = next( + ( + i + for i in range(len(segments) - 2, 0, -1) + if segments[i] in {"414", "417"} and len(segments) - i in {2, 4} + ), + -1, + ) + if ix < 0: + return [ + issue + for issue in ( + query_issue, + ValidationIssue( + path, "GS1_GLN_INVALID", "A GS1 GLN Digital Link must use AI 414 or AI 417" + ), + ) + if issue is not None + ] + ai, key = segments[ix], segments[ix + 1] + if len(segments) - ix == 4 and ( + ai != "414" or segments[ix + 2] != "254" or not _xchar_value(segments[ix + 3], 20) + ): + return [ + issue + for issue in ( + query_issue, + ValidationIssue( + path, + "GS1_GLN_INVALID", + "Only physical-location GLN can carry a GS1 GLN extension", + ), + ) + if issue is not None + ] + elif len(identifier) == 16 and identifier[:3] in {"414", "417"}: + ai, key = identifier[:3], identifier[3:] + else: + return [ValidationIssue(path, "GS1_GLN_INVALID", "A GS1 GLN must include AI 414 or AI 417")] + if not _gs1_check_digit(key, 13): + return [ + issue + for issue in ( + query_issue, + ValidationIssue( + path, + "GS1_GLN_INVALID", + "A GLN must be 13 digits and have a valid GS1 check digit", + ), + ) + if issue is not None + ] + expected = "417" if role == "party" else "414" + if ai is not None and ai != expected: + role_label = "party (AI 417)" if role == "party" else "physical location (AI 414)" + role_issue = ValidationIssue( + path, "GS1_GLN_ROLE_MISMATCH", f"Identifier must identify a {role_label}" ) + else: + role_issue = None + return [issue for issue in (query_issue, role_issue) if issue is not None] + +class IdentifierValidator: + """Validates explicitly selected EN 18219 local identifier profiles.""" -def validate_dpp_core(core: DppCore | None) -> None: - """Validate the reusable DPP core. Fail-fast; raises on first violation.""" - if core is None: - raise DppValidationError("DppCore cannot be null") - validate_passport_metadata(core.passportMetadata) - validate_nameplate(core.nameplate) - validate_documentation(core.documentation) + def validate( + self, header: DppHeader | None, context: IdentifierValidationContext | None + ) -> ValidationResult: + if header is None: + return ValidationResult( + [ValidationIssue("header", "IDENTIFIER_HEADER_REQUIRED", "DppHeader is required")] + ) + if context is None: + return ValidationResult( + [ + ValidationIssue( + "context", + "IDENTIFIER_VALIDATION_CONTEXT_REQUIRED", + "IdentifierValidationContext is required", + ) + ] + ) + issues: list[ValidationIssue] = [] + if context.product_identifier_method is ProductIdentifierMethod.GS1_DIGITAL_LINK: + issues.extend(_gs1_product(header.unique_product_identifier, header.granularity)) + elif context.product_identifier_method is ProductIdentifierMethod.IDENTIFICATION_LINK: + identifier = header.unique_product_identifier or "" + permitted = ( + re.fullmatch(r"[A-Za-z0-9._~:/?#\[\]@!$&'()*+,;=-]+", identifier) is not None + ) + if not identifier or len(identifier) > 255 or not permitted: + issues.append( + ValidationIssue( + "header.uniqueProductIdentifier", + "IDENTIFICATION_LINK_URL_INVALID", + "Identification Link must use valid IEC 61406-1 URL syntax and " + "permitted characters", + ) + ) + else: + scheme = re.match(r"^[A-Za-z][A-Za-z0-9+.-]*:", identifier) + explicit_authority = ( + re.match(r"^[A-Za-z][A-Za-z0-9+.-]*://", identifier) is not None + ) + authority_end = min( + (identifier.find(char) for char in "/?#" if identifier.find(char) >= 0), + default=len(identifier), + ) + authority = identifier[:authority_end] + port = re.search(r":([0-9]+)$", authority) + if not explicit_authority and port is not None or scheme is None: + normalized = "https://" + identifier + else: + normalized = identifier + try: + parsed = urlsplit(normalized) + if ( + parsed.scheme.lower() != "https" + or not parsed.hostname + or parsed.fragment + or any( + label.lower().startswith("xn--") for label in parsed.hostname.split(".") + ) + ): + raise ValueError + _ = parsed.port + except ValueError: + issues.append( + ValidationIssue( + "header.uniqueProductIdentifier", + "IDENTIFICATION_LINK_URL_INVALID", + "Identification Link must use valid IEC 61406-1 URL syntax and " + "permitted characters", + ) + ) + else: + parameters = parsed.query.split("&") if parsed.query else [] + sidi = [p for p in parameters if p.split("=", 1)[0].startswith(".")] + if not sidi: + issue = _granularity_issue(Granularity.ITEM, header.granularity) + if issue: + issues.append(issue) + else: + malformed_parameter = any( + ("=" not in parameter or not parameter.split("=", 1)[0]) + for parameter in parameters + if not parameter.split("=", 1)[0].startswith(".") + ) + if malformed_parameter: + issues.append( + ValidationIssue( + "header.uniqueProductIdentifier", + "IDENTIFICATION_LINK_STRUCTURED_SYNTAX_INVALID", + "Structured query parameters must be name/value pairs", + ) + ) + else: + product = batch = item = False + for parameter in sidi: + name, sep, value = parameter.partition("=") + if not sep or re.fullmatch(r"\.[0-9]{0,3}[A-Z]", name) is None: + issues.append( + ValidationIssue( + "header.uniqueProductIdentifier", + "IDENTIFICATION_LINK_STRUCTURED_SYNTAX_INVALID", + "SIDI parameters require a dot-prefixed " + "Data Identifier and a name/value pair", + ) + ) + break + if not value: + issues.append( + ValidationIssue( + "header.uniqueProductIdentifier", + "IDENTIFICATION_LINK_REQUIRED_IDENTIFIER_MISSING", + "A structured Data Identifier requires a " + "non-empty value", + ) + ) + break + if name in {".P", ".1P"}: + product = True + elif name in {".T", ".1T"}: + batch = True + elif name == ".S": + item = True + else: + issues.append( + ValidationIssue( + "header.uniqueProductIdentifier", + "IDENTIFICATION_LINK_DI_UNSUPPORTED", + "This locally validated structured subset recognizes " + "product categories .P/.1P, batch categories .T/.1T, " + "and serial category .S", + ) + ) + break + else: + represented = ( + Granularity.ITEM + if item + else Granularity.BATCH + if batch and product + else Granularity.MODEL + if product + else None + ) + if represented is None: + issues.append( + ValidationIssue( + "header.uniqueProductIdentifier", + "IDENTIFICATION_LINK_REQUIRED_IDENTIFIER_MISSING", + "A structured product link requires product-code, " + "batch, or item identification", + ) + ) + else: + issue = _granularity_issue(represented, header.granularity) + if issue: + issues.append(issue) + else: + issues.append( + ValidationIssue( + "header.uniqueProductIdentifier", + "IDENTIFIER_METHOD_UNSUPPORTED", + "The selected product identifier method is outside this SDK milestone", + ) + ) + + if ( + context.economic_operator_identifier_method + is EconomicOperatorIdentifierMethod.UNSUPPORTED + ): + issues.append( + ValidationIssue( + "header.economicOperatorId", + "IDENTIFIER_METHOD_UNSUPPORTED", + "The selected economic-operator identifier method is outside this SDK " + "milestone", + ) + ) + else: + issues.extend(_gln(header.economic_operator_id, "header.economicOperatorId", "party")) + if header.facility_id is not None: + if context.facility_identifier_method is not FacilityIdentifierMethod.GS1_GLN: + issues.append( + ValidationIssue( + "header.facilityId", + "IDENTIFIER_METHOD_UNSUPPORTED", + "The selected facility identifier method is outside this SDK milestone", + ) + ) + else: + issues.extend(_gln(header.facility_id, "header.facilityId", "location")) + return ValidationResult(issues) diff --git a/src/dpp_sdk/dpp4fun/README.md b/src/dpp_sdk/dpp4fun/README.md index 08e92e9..9cfdf25 100644 --- a/src/dpp_sdk/dpp4fun/README.md +++ b/src/dpp_sdk/dpp4fun/README.md @@ -1,132 +1,12 @@ # DPP SDK DPP4Fun module -## Purpose - -`dpp_sdk.dpp4fun` adds the furniture information to a reusable passport. Use it when your product -is furniture and you want the ready-made `Dpp4Fun` model, its checks, and its JSON conversion. It -uses the reusable core models and does not depend on repository or registry clients. - -It does not implement a generic DPP standard for every product sector, an HTTP service, persistence, -or a standalone reusable-core JSON codec. - -## Architecture at a glance - -![DPP4Fun aggregate boundary](../../../docs/architecture/python-dpp4fun-model.svg) - -`dpp_sdk.dpp4fun` adds a furniture aggregate, its explicit semantic validation, and flat/nested -JSON mapping. It depends on reusable Core contracts and remains independent of the HTTP clients. - -The models are frozen Pydantic models. Contracted collections are immutable tuples in memory and -JSON arrays on the wire. - -## Public surface - -| Need | Public types or function | -| --- | --- | -| Complete furniture passport | `Dpp4Fun` | -| Classification and characteristics | `ProductClassification`, `Characteristics`, `Dimensions` | -| Bill of Materials | `BillOfMaterials`, `Material`, `Component`, `Part` | -| JSON codec | `Dpp4FunJsonCodec`, `to_json()`, `from_json()`, `from_json_and_validate()` | -| Explicit semantic checks | `validate_dpp4fun()` | - -`Dpp4Fun` inherits the core identifier accessors: `dpp_id` and `product_id`. - -The repository [model guide](../../../docs/model-guide.md) owns the complete DPP4Fun and Bill of -Materials field/default/null tables. The [validation guide](../../../docs/validation-guide.md) and -[validation-rule reference](../../../docs/validation-rules.md) own the explicit validation and -codec failure boundary. - -## Practical usage - -The root [SDK usage guide](../../../docs/usage.md) owns the complete construction walkthrough. This -module example shows the aggregate codec and explicit validation boundary: - -```python -from dpp_sdk.dpp4fun import from_json, to_json, validate_dpp4fun - -raw = """{ - "passportMetadata": { - "uniqueProductIdentifier": "11111111-1111-1111-1111-111111111111", - "passportUpdateDates": ["2026-06-29"] - }, - "nameplate": { - "gtinCode": "04012345678901", - "manufacturer": {"name": "Cir4Fun Furniture GmbH", "role": "MANUFACTURER"} - }, - "classification": {"sector": "Furniture", "category": "Seating", "tags": []}, - "characteristics": {"productName": "ErgoChair", "features": []} -}""" - -dpp = from_json(raw) # structural mapping only -validate_dpp4fun(dpp) # explicit semantic validation -outgoing_json = to_json(dpp) # canonical flattened output -assert from_json(outgoing_json) == dpp -``` - -`from_json_and_validate()` is available when an application needs those two stages together. - -## Immutable updates - -```python -updated = dpp.with_updates( - characteristics=dpp.characteristics.with_updates(productName="ErgoChair Pro") -) -validate_dpp4fun(updated) -assert dpp.characteristics.productName == "ErgoChair" -``` - -Use `with_updates()` rather than unchecked `model_copy(update=...)` for public updates. - -## JSON and validation behavior - -Outbound JSON flattens `passportMetadata`, `nameplate`, and optional `documentation` from -`coreDpp`. Inbound mapping accepts that flat shape and the supported nested `coreDpp` shape. Mapping -does not implicitly apply semantic validation. - -`from_json()` rejects non-object roots, null roots, malformed structure, null list members, and -non-finite JSON numeric values through `DppMappingError`. Semantic validation is fail-fast through -`DppValidationError` and includes core, classification, characteristics, Bill of Materials, and -cross-object rules. - -Required and optional text follow the core module's frozen Unicode-whitespace contract; U+200B is -visible text rather than blank. Dimensions, material portions, and weight must be finite and -non-negative. `tags` and `features` require clean string members: a JSON null member is a mapping -failure, while defensive semantic validation reports an indexed error. Validation checks core, -classification, characteristics, Bill of Materials, and cross-rules in fail-fast order; each BOM -member is validated before duplicate/reference and aggregate rules are applied. - -Failure stages stay distinct: Pydantic construction rejects invalid model shape, explicit semantic -validation raises `DppValidationError`, and JSON normalization or model mapping raises -`DppMappingError`. Consumers should not rely on a fail-fast validator to accumulate every defect. - -## Build and test locally - -**Purpose:** run focused furniture-model, validation, and codec checks. **Run from:** repository -root. **Prerequisites:** the checkout development environment. The -[release guide](../../../RELEASING.md) owns installation, full validation, builds, archive -inspection, and cleanup. - -### PowerShell - -```powershell -.\.venv\Scripts\python.exe -m pytest tests/test_dpp4fun_validation.py tests/test_transport_roundtrip.py tests/test_end_to_end.py -``` - -### Linux/macOS - -```bash -.venv/bin/python -m pytest tests/test_dpp4fun_validation.py tests/test_transport_roundtrip.py tests/test_end_to_end.py -``` - -**Expected result:** focused model, validation, and transport tests pass. **Cleanup:** none. - -## Boundaries and limitations - -- The DPP4Fun contract is furniture-specific; it is not a generic model for every DPP sector. -- The codec supports the documented flat and nested input forms only; it does not accept arbitrary - JSON or return a nullable root value. -- The module does not implement HTTP clients, repositories, registries, persistence, Docker, Spring, - EDC, or standards certification. - -Next: [Clients module](../clients/README.md) for public HTTP consumers, or -[SDK usage](../../../docs/usage.md) for the complete consumer path. +`dpp_sdk.dpp4fun` is a typed view over a core passport for the DPP4Fun furniture profile (D4.1): +19 aspects, six product types, reusable composites, unit-safe quantities, profile validation +(`validate_dpp4fun()`), and `Dpp4FunJsonCodec`. The bundled +`semantic/dpp4fun-semantic-profile-v1.json` describes all 488 fields; Python reads that packaged +copy at runtime. + +- [Model guide](../../../docs/model-guide.md#build-a-dpp4fun-passport): build, read, edit, and validate. +- [DPP4Fun field reference](../../../docs/dpp4fun-fields.md): every field name per aspect. +- [Standards alignment and validation rules](../../../docs/conformance.md): D4.1 source conflicts. +- [Contributing](../../../CONTRIBUTING.md): tests and checks for this module. diff --git a/src/dpp_sdk/dpp4fun/__init__.py b/src/dpp_sdk/dpp4fun/__init__.py index f32f9dc..7cb1b3e 100644 --- a/src/dpp_sdk/dpp4fun/__init__.py +++ b/src/dpp_sdk/dpp4fun/__init__.py @@ -1,33 +1,152 @@ -"""Furniture-specific DPP aggregate, validation, and JSON transport. +"""Typed DPP4Fun domain profile over the reusable core passport model.""" -Furniture-specific DPP aggregate, validation, and JSON transport. -""" - -from .model import ( - BillOfMaterials, - Characteristics, +from .characteristics import ( + ChairCharacteristics, + CribCharacteristics, + Dpp4FunProductVariant, + FabricCharacteristics, + FurnitureCharacteristics, + LockerCharacteristics, + ParticleBoardCharacteristics, + SofaCharacteristics, +) +from .composites import ( + Accessory, + Address, + CarbonFootprintCalculation, + Certificate, + CollectionMember, Component, - Dimensions, - Dpp4Fun, + Contact, + DesignRecord, + Email, + HealthIndicator, + Indicator, + Instruction, + ManufacturingRecord, Material, Part, - ProductClassification, + Policies, + PostSaleService, + ProductFunction, + RepairRecord, + Scoring, + StandardAssessment, + Telephone, + TrackingRecord, +) +from .profile import ( + DPP4FUN_PROFILE, + Dpp4FunElementDefinition, + Dpp4FunSemanticProfile, + Dpp4FunSemanticResolver, + profile_definition, ) from .transport import Dpp4FunJsonCodec, from_json, from_json_and_validate, to_json -from .validation import validate_dpp4fun +from .typed_model import ( + BillOfMaterials, + CarbonFootprint, + Dpp4Fun, + Durability, + EnvironmentalImpact, + HealthImpact, + Nameplate, + ProductRelationship, + ProfiledAspect, + Recyclability, + Refurbishment, + Reliability, + Remanufacturing, + Repairability, + Reusability, + RightOfEndUsers, + Safety, + Traceability, + Upgradability, + Usage, + WasteManagement, +) +from .units import ( + CustomSemanticUnit, + Dpp4FunQuantity, + LengthUnit, + MassUnit, + SemanticUnit, + UnitDimension, + VolumeUnit, +) +from .validation import Dpp4FunValidationService, Dpp4FunValidator, validate_dpp4fun __all__ = [ + "Accessory", + "Address", "BillOfMaterials", - "Characteristics", + "CarbonFootprint", + "CarbonFootprintCalculation", + "Certificate", + "ChairCharacteristics", + "CollectionMember", "Component", - "Dimensions", + "Contact", + "CribCharacteristics", + "CustomSemanticUnit", + "DPP4FUN_PROFILE", + "DesignRecord", "Dpp4Fun", "Dpp4FunJsonCodec", + "Dpp4FunProductVariant", + "Dpp4FunSemanticResolver", + "Dpp4FunValidationService", + "Dpp4FunValidator", + "Durability", + "Email", + "EnvironmentalImpact", + "FabricCharacteristics", + "FurnitureCharacteristics", + "HealthImpact", + "HealthIndicator", + "Indicator", + "Instruction", + "LengthUnit", + "LockerCharacteristics", + "ManufacturingRecord", + "MassUnit", "Material", + "Nameplate", "Part", - "ProductClassification", + "ParticleBoardCharacteristics", + "Policies", + "PostSaleService", + "ProductFunction", + "ProductRelationship", + "Dpp4FunElementDefinition", + "ProfiledAspect", + "Recyclability", + "Refurbishment", + "Reliability", + "Remanufacturing", + "RepairRecord", + "Repairability", + "Reusability", + "RightOfEndUsers", + "Safety", + "Scoring", + "Dpp4FunQuantity", + "Dpp4FunSemanticProfile", + "SemanticUnit", + "SofaCharacteristics", + "StandardAssessment", + "Telephone", + "Traceability", + "TrackingRecord", + "UnitDimension", + "Upgradability", + "Usage", + "VolumeUnit", + "WasteManagement", "from_json", "from_json_and_validate", + "profile_definition", "to_json", "validate_dpp4fun", ] diff --git a/src/dpp_sdk/dpp4fun/characteristics.py b/src/dpp_sdk/dpp4fun/characteristics.py new file mode 100644 index 0000000..8f72e18 --- /dev/null +++ b/src/dpp_sdk/dpp4fun/characteristics.py @@ -0,0 +1,55 @@ +"""The six source-specific D4.1 Characteristics profile variants.""" + +from enum import StrEnum + +from .typed_model import ProfiledAspect + + +class Dpp4FunProductVariant(StrEnum): + CHAIR = "chair" + CRIB = "crib" + FABRIC = "fabric" + LOCKER = "locker" + PARTICLE_BOARD = "particleBoard" + SOFA = "sofa" + + +class ChairCharacteristics(ProfiledAspect): + element_id = "characteristics" + variant = Dpp4FunProductVariant.CHAIR + + +class CribCharacteristics(ProfiledAspect): + element_id = "characteristics" + variant = Dpp4FunProductVariant.CRIB + field_aliases = {"mattressIncluded": "matelasIncluded"} + + +class FabricCharacteristics(ProfiledAspect): + element_id = "characteristics" + variant = Dpp4FunProductVariant.FABRIC + + +class LockerCharacteristics(ProfiledAspect): + element_id = "characteristics" + variant = Dpp4FunProductVariant.LOCKER + + +class ParticleBoardCharacteristics(ProfiledAspect): + element_id = "characteristics" + variant = Dpp4FunProductVariant.PARTICLE_BOARD + + +class SofaCharacteristics(ProfiledAspect): + element_id = "characteristics" + variant = Dpp4FunProductVariant.SOFA + + +FurnitureCharacteristics = ( + ChairCharacteristics + | CribCharacteristics + | FabricCharacteristics + | LockerCharacteristics + | ParticleBoardCharacteristics + | SofaCharacteristics +) diff --git a/src/dpp_sdk/dpp4fun/composites.py b/src/dpp_sdk/dpp4fun/composites.py new file mode 100644 index 0000000..239526b --- /dev/null +++ b/src/dpp_sdk/dpp4fun/composites.py @@ -0,0 +1,113 @@ +"""Immutable typed views for reusable D4.1 composite DataElement collections.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import ClassVar, Self + +from ..core.model import DataElementCollection +from .profile import profile_definition +from .typed_model import CoreElement, _element_for, _ordered_children + + +@dataclass(frozen=True, slots=True) +class _Composite: + element: DataElementCollection + element_id: ClassVar[str] + profile_path: ClassVar[str] + field_aliases: ClassVar[dict[str, str]] = {} + + @classmethod + def create(cls, **fields: object) -> Self: + try: + root = profile_definition(cls.profile_path) + except KeyError: + if fields: + raise + root = None + children = tuple( + _element_for( + profile_definition(f"{cls.profile_path}.{cls.field_aliases.get(name, name)}"), + value, + ) + for name, value in fields.items() + if value is not None + ) + children = _ordered_children(cls.profile_path, children) + return cls( + element=DataElementCollection( + element_id=root.element_id if root else cls.element_id, + object_type="DataElementCollection", + dictionary_reference=root.dictionary_reference if root else None, + elements=children, + ) + ) + + @classmethod + def from_element(cls, element: DataElementCollection) -> Self: + return cls(element=element) + + def as_data_element(self) -> DataElementCollection: + return self.element + + def field(self, name: str) -> CoreElement | None: + definition = profile_definition(f"{self.profile_path}.{self.field_aliases.get(name, name)}") + return next( + (child for child in self.element.elements if child.element_id == definition.element_id), + None, + ) + + def with_field(self, name: str, value: object | None) -> Self: + definition = profile_definition(f"{self.profile_path}.{self.field_aliases.get(name, name)}") + retained = tuple( + child for child in self.element.elements if child.element_id != definition.element_id + ) + children = retained if value is None else (*retained, _element_for(definition, value)) + children = _ordered_children(self.profile_path, children) + return type(self)(element=self.element.with_updates(elements=children)) + + +def _composite(name: str, element_id: str, profile_path: str) -> type[_Composite]: + return type( + name, + (_Composite,), + {"element_id": element_id, "profile_path": profile_path, "__module__": __name__}, + ) + + +Accessory = _composite("Accessory", "accessory", "productRelationship.accessorySet[*]") +Address = _composite("Address", "address", "nameplate.contact.address") +CollectionMember = _composite( + "CollectionMember", "collectionMember", "productRelationship.collectionMemberSet[*]" +) +Certificate = _composite("Certificate", "certificate", "durability.certificateSet[*]") +CarbonFootprintCalculation = _composite( + "CarbonFootprintCalculation", + "carbonFootprintCalculation", + "carbonFootprint.carbonFootprintCalculationSet[*]", +) +DesignRecord = _composite("DesignRecord", "designRecord", "traceability.designRecordSet[*]") +Contact = _composite("Contact", "contact", "nameplate.contact") +Email = _composite("Email", "email", "nameplate.contact.email") +HealthIndicator = _composite("HealthIndicator", "indicator", "healthImpact.indicatorSet[*]") +Component = _composite("Component", "component", "billOfMaterials.componentSet[*]") +ManufacturingRecord = _composite( + "ManufacturingRecord", "manufacturingRecord", "traceability.manufacturingRecordSet[*]" +) +Instruction = _composite("Instruction", "assemblyInstruction", "usage.assemblyInstructionSet[*]") +Indicator = _composite("Indicator", "indicator", "healthImpact.indicatorSet[*]") +Part = _composite("Part", "part", "billOfMaterials.partSet[*]") +Material = _composite("Material", "material", "billOfMaterials.materialSet[*]") +Policies = _composite("Policies", "policies", "rightOfEndUsers.policies") +Policies.field_aliases = {"permissions": "permissionSet", "prohibitions": "prohibitionSet"} +PostSaleService = _composite( + "PostSaleService", "postSaleService", "rightOfEndUsers.postSaleServiceSet[*]" +) +ProductFunction = _composite("ProductFunction", "primaryFunction", "usage.primaryFunction") +TrackingRecord = _composite("TrackingRecord", "trackingRecord", "traceability.trackingRecordSet[*]") +Telephone = _composite("Telephone", "telephone", "nameplate.contact.telephone") +StandardAssessment = _composite( + "StandardAssessment", "standardAssessment", "durability.standardSet[*]" +) +RepairRecord = _composite("RepairRecord", "repairRecord", "repairability.repairRecordSet[*]") +Scoring = _composite("Scoring", "scoring", "durability.scoringSet[*]") diff --git a/src/dpp_sdk/dpp4fun/model.py b/src/dpp_sdk/dpp4fun/model.py deleted file mode 100644 index ad8f97b..0000000 --- a/src/dpp_sdk/dpp4fun/model.py +++ /dev/null @@ -1,147 +0,0 @@ -"""Furniture-specific DPP models (port of ``dppsdk.dpp4fun.model``). - -Frozen Pydantic v2 models that double as JSON transport payloads. List fields -(``tags``, ``features``, BOM lists) default to ``[]`` and are always emitted — -mirroring the Java domain objects, which store empty lists rather than null, so -the wire shape carries ``[]`` not ``null``. - -``Dimensions`` requires all three of width/height/depth (the Java builder rejects -any missing value, and the mapper only succeeds when all are present). - -Only structural rules are enforced here; semantic rules live in -:mod:`dpp_sdk.dpp4fun.validation`. -""" - -from __future__ import annotations - -from typing import Annotated - -from pydantic import Field - -from ..core.model import ( - Dpp, - DppCore, - NonBlankStr, - OptionalStr, - _Base, -) - -# A non-negative float, used for dimensions, weight, and material portion. -NonNegativeFloat = Annotated[float, Field(ge=0, allow_inf_nan=False)] - - -class Dimensions(_Base): - width: NonNegativeFloat - height: NonNegativeFloat - depth: NonNegativeFloat - unit: OptionalStr = None - - -class Material(_Base): - name: NonBlankStr - mandatory: bool = False - portion: NonNegativeFloat = 0.0 - reference: OptionalStr = None - - -class Component(_Base): - name: NonBlankStr - reference: OptionalStr = None - - -class Part(_Base): - name: NonBlankStr - mandatory: bool = False - reference: OptionalStr = None - - -class BillOfMaterials(_Base): - materials: tuple[Material, ...] = Field(default_factory=tuple) - components: tuple[Component, ...] = Field(default_factory=tuple) - parts: tuple[Part, ...] = Field(default_factory=tuple) - - -class ProductClassification(_Base): - sector: NonBlankStr - group: OptionalStr = None - category: NonBlankStr - subCategory: OptionalStr = None - tags: tuple[str, ...] = Field(default_factory=tuple) - - -class Characteristics(_Base): - productName: NonBlankStr - description: OptionalStr = None - brand: OptionalStr = None - productType: OptionalStr = None - dimensions: Dimensions | None = None - weight: NonNegativeFloat | None = None - color: OptionalStr = None - features: tuple[str, ...] = Field(default_factory=tuple) - - -class Dpp4Fun(Dpp): - """Complete furniture Digital Product Passport (port of ``Dpp4Fun``).""" - - coreDpp: DppCore - classification: ProductClassification - characteristics: Characteristics - billOfMaterials: BillOfMaterials | None = None - - @property - def passport_type(self) -> str: - return "Dpp4Fun Furniture" - - # --- read-through accessors (classification) -------------------------------- - @property - def sector(self) -> str: - return self.classification.sector - - @property - def group(self) -> str | None: - return self.classification.group - - @property - def category(self) -> str: - return self.classification.category - - @property - def subCategory(self) -> str | None: - return self.classification.subCategory - - @property - def tags(self) -> tuple[str, ...]: - return self.classification.tags - - # --- read-through accessors (characteristics) ------------------------------- - @property - def productName(self) -> str: - return self.characteristics.productName - - @property - def description(self) -> str | None: - return self.characteristics.description - - @property - def brand(self) -> str | None: - return self.characteristics.brand - - @property - def productType(self) -> str | None: - return self.characteristics.productType - - @property - def dimensions(self) -> Dimensions | None: - return self.characteristics.dimensions - - @property - def weight(self) -> float | None: - return self.characteristics.weight - - @property - def color(self) -> str | None: - return self.characteristics.color - - @property - def features(self) -> tuple[str, ...]: - return self.characteristics.features diff --git a/src/dpp_sdk/dpp4fun/profile.py b/src/dpp_sdk/dpp4fun/profile.py new file mode 100644 index 0000000..7976d27 --- /dev/null +++ b/src/dpp_sdk/dpp4fun/profile.py @@ -0,0 +1,189 @@ +"""Immutable view of the reviewed, packaged DPP4Fun semantic profile.""" + +from __future__ import annotations + +import json +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from importlib.resources import files +from types import MappingProxyType +from typing import Any + +from ..core.semantic import DataElementKind, SemanticDefinition, SemanticPath + + +def _freeze(value: Any) -> Any: + if isinstance(value, dict): + return MappingProxyType({key: _freeze(item) for key, item in value.items()}) + if isinstance(value, list): + return tuple(_freeze(item) for item in value) + return value + + +@dataclass(frozen=True, slots=True) +class Dpp4FunElementDefinition: + path: str + element_id: str + object_type: str + dictionary_reference: str | None + value_data_type: str | None + source_cardinality: Mapping[str, int | None] | None + binding_occurrence: Mapping[str, int | None] + unit_dimension: str | None + unit_slot_path: str | None + applies_to: tuple[str, ...] + aliases: tuple[str, ...] + source: Mapping[str, Any] + java_concept: str + source_references: tuple[Mapping[str, Any], ...] + validation_rules: tuple[str, ...] + repeated_item_policy: Mapping[str, Any] | None + resolution_status: str + conflict_resolution: Mapping[str, Any] | None + explicit_requiredness: str + + +@dataclass(frozen=True, slots=True) +class Dpp4FunSemanticProfile: + metadata: Mapping[str, Any] + definitions: tuple[Dpp4FunElementDefinition, ...] + variants: Mapping[str, str] + conflicts: tuple[Mapping[str, Any], ...] + by_path: Mapping[str, tuple[Dpp4FunElementDefinition, ...]] + + @classmethod + def load(cls) -> Dpp4FunSemanticProfile: + resource = files("dpp_sdk.dpp4fun").joinpath("semantic/dpp4fun-semantic-profile-v1.json") + raw = json.loads(resource.read_text(encoding="utf-8")) + definitions = tuple( + Dpp4FunElementDefinition( + path=item["path"], + element_id=item["elementId"], + object_type=item["objectType"], + dictionary_reference=item["dictionaryReference"], + value_data_type=item["valueDataType"], + source_cardinality=_freeze(item["sourceCardinality"]), + binding_occurrence=_freeze(item["bindingOccurrence"]), + unit_dimension=item["unitDimension"], + unit_slot_path=item["unitSlotPath"], + applies_to=tuple(item["appliesTo"]), + aliases=tuple(item["aliases"]), + source=_freeze(item["source"]), + java_concept=item["javaConcept"], + source_references=tuple(_freeze(ref) for ref in item["sourceReferences"]), + validation_rules=tuple(item["validationRules"]), + repeated_item_policy=_freeze(item["repeatedItemPolicy"]), + resolution_status=item["resolutionStatus"], + conflict_resolution=_freeze(item["conflictResolution"]), + explicit_requiredness=item["explicitRequiredness"], + ) + for item in raw["definitions"] + ) + grouped: dict[str, list[Dpp4FunElementDefinition]] = {} + for definition in definitions: + grouped.setdefault(definition.path, []).append(definition) + return cls( + metadata=_freeze({key: value for key, value in raw.items() if key != "definitions"}), + definitions=definitions, + variants=MappingProxyType(dict(raw["variantContentSpecificationIds"])), + conflicts=tuple(_freeze(item) for item in raw["conflictDecisions"]), + by_path=MappingProxyType({key: tuple(items) for key, items in grouped.items()}), + ) + + @property + def root_element_ids(self) -> tuple[str, ...]: + return tuple( + dict.fromkeys( + definition.element_id + for definition in self.definitions + if "." not in definition.path + ) + ) + + +DPP4FUN_PROFILE = Dpp4FunSemanticProfile.load() + + +def profile_definition(path: str, *, variant: str | None = None) -> Dpp4FunElementDefinition: + candidates = DPP4FUN_PROFILE.by_path.get(path, ()) + if variant is not None: + key = f"characteristics-{variant}" + candidates = tuple(item for item in candidates if key in item.applies_to) + elif len(candidates) > 1: + candidates = tuple(item for item in candidates if "general" in item.applies_to) + if len(candidates) != 1: + raise KeyError(f"No unique DPP4Fun profile definition for {path!r} (variant={variant!r})") + return candidates[0] + + +class Dpp4FunSemanticResolver: + """Resolve only metadata explicitly present in the selected DPP4Fun profile.""" + + def resolve( + self, content_specification_ids: Sequence[str], path: SemanticPath + ) -> SemanticDefinition | None: + ids = set(content_specification_ids) + applicability = ( + {"general"} if DPP4FUN_PROFILE.metadata["contentSpecificationId"] in ids else set() + ) + applicability.update( + f"characteristics-{variant}" + for variant, identifier in DPP4FUN_PROFILE.variants.items() + if identifier in ids + ) + match = next( + ( + definition + for definition in DPP4FUN_PROFILE.definitions + if definition.applies_to + and applicability.intersection(definition.applies_to) + and _profile_path_matches(definition.path, path.segments) + ), + None, + ) + if match is None: + return None + item_pattern = ( + match.repeated_item_policy.get("elementIdPattern") + if match.repeated_item_policy + else None + ) + element_id = match.element_id + if item_pattern: + index = next(int(segment) for segment in path.segments if segment.isdecimal()) + element_id = item_pattern % index + kind = { + "DataElementCollection": DataElementKind.COLLECTION, + "SingleValuedDataElement": DataElementKind.SINGLE, + "MultiValuedDataElement": DataElementKind.MULTI_VALUED, + "MultiLanguageDataElement": DataElementKind.MULTI_LANGUAGE, + "RelatedResource": DataElementKind.RELATED_RESOURCE, + }[match.object_type] + return SemanticDefinition( + kind=kind, + element_id=element_id, + dictionary_reference=match.dictionary_reference, + value_data_type=match.value_data_type, + dictionary_reference_resolved=match.resolution_status + not in {"UNRESOLVED_DICTIONARY_REFERENCE", "RESOLVED_WITHOUT_DICTIONARY_REFERENCE"}, + value_data_type_resolved=match.resolution_status != "UNRESOLVED_VALUE_DATA_TYPE", + ) + + +def _profile_path_matches(pattern: str, segments: tuple[str, ...]) -> bool: + expected = pattern.split(".") + actual_index = 0 + for segment in expected: + if actual_index >= len(segments): + return False + if segment.endswith("[*]"): + if segment[:-3] != segments[actual_index] or actual_index + 1 >= len(segments): + return False + if not segments[actual_index + 1].isdecimal(): + return False + actual_index += 2 + elif segment != segments[actual_index]: + return False + else: + actual_index += 1 + return actual_index == len(segments) diff --git a/src/dpp_sdk/dpp4fun/semantic/dpp4fun-semantic-profile-v1.json b/src/dpp_sdk/dpp4fun/semantic/dpp4fun-semantic-profile-v1.json new file mode 100644 index 0000000..e64d24d --- /dev/null +++ b/src/dpp_sdk/dpp4fun/semantic/dpp4fun-semantic-profile-v1.json @@ -0,0 +1,25507 @@ +{ + "contentSpecificationId": "DPP4FUN_COMMON_V1", + "variantContentSpecificationIds": { + "chair": "DPP4FUN_CHAIR_V1", + "crib": "DPP4FUN_CRIB_V1", + "fabric": "DPP4FUN_FABRIC_V1", + "locker": "DPP4FUN_LOCKER_V1", + "particleBoard": "DPP4FUN_PARTICLE_BOARD_V1", + "sofa": "DPP4FUN_SOFA_V1" + }, + "sources": { + "d4Version": "1.0", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "ontologyVersion": "0.2.2" + }, + "profileVersion": "1.0.0", + "authority": "D4.1 v1.0, EN 18223:2026, EN 18222:2026, and DPP4Fun 0.2.2 templates pinned at 2007a574b0e35eae5997c9460bf4cf531588405b", + "definitions": [ + { + "path": "billOfMaterials", + "elementId": "billOfMaterials", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#BillOfMaterials", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "BillOfMaterials", + "https://www.cir4fun.eu/ontology/dpp4fun#BillOfMaterials" + ], + "source": { + "aasModelType": "Submodel", + "d4Name": "BillOfMaterials", + "aasPath": "BillOfMaterials", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "EN root elementId uniqueness; D4.1 aspect presence not mandated" + }, + "javaConcept": "BillOfMaterials", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.1", + "figureNumber": 3, + "aasTemplate": "CIR4FUN.BillOfMaterials.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "billOfMaterials.materialSet", + "elementId": "materialSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "MaterialSet", + "BillOfMaterials.MaterialSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "MaterialSet", + "aasPath": "BillOfMaterials.MaterialSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "BillOfMaterials.materialSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.1", + "figureNumber": 3, + "aasTemplate": "CIR4FUN.BillOfMaterials.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "billOfMaterials.materialSet[*]", + "elementId": "material", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#MaterialInformation", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Material", + "BillOfMaterials.MaterialSet.Material_00", + "http://www.w3id.org/pdto#MaterialInformation" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "Material", + "aasPath": "BillOfMaterials.MaterialSet.Material_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "BillOfMaterials.materialSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.1", + "figureNumber": 3, + "aasTemplate": "CIR4FUN.BillOfMaterials.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "material_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "billOfMaterials.materialSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "BillOfMaterials.MaterialSet.Material_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "BillOfMaterials.MaterialSet.Material_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "BillOfMaterials.materialSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.1", + "figureNumber": 3, + "aasTemplate": "CIR4FUN.BillOfMaterials.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "billOfMaterials.materialSet[*].mandatory", + "elementId": "mandatory", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#isMandatory", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Mandatory", + "BillOfMaterials.MaterialSet.Material_00.Mandatory", + "http://www.w3id.org/pdto#isMandatory" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Mandatory", + "aasPath": "BillOfMaterials.MaterialSet.Material_00.Mandatory", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "BillOfMaterials.materialSet[].mandatory", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.1", + "figureNumber": 3, + "aasTemplate": "CIR4FUN.BillOfMaterials.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "billOfMaterials.materialSet[*].portion", + "elementId": "portion", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasPortion", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Portion", + "BillOfMaterials.MaterialSet.Material_00.Portion", + "http://www.w3id.org/pdto#hasPortion" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "D4.1 prose+UML+ontology samples Double; AAS xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Portion", + "aasPath": "BillOfMaterials.MaterialSet.Material_00.Portion", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "BillOfMaterials.materialSet[].portion", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.1", + "figureNumber": 3, + "aasTemplate": "CIR4FUN.BillOfMaterials.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "D4.1 prose+UML+ontology samples Double; AAS xs:string" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "billOfMaterials.materialSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "BillOfMaterials.MaterialSet.Material_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "Reference", + "aasPath": "BillOfMaterials.MaterialSet.Material_00.Reference", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "BillOfMaterials.materialSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.1", + "figureNumber": 3, + "aasTemplate": "CIR4FUN.BillOfMaterials.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "billOfMaterials.componentSet", + "elementId": "componentSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ComponentSet", + "BillOfMaterials.ComponentSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "ComponentSet", + "aasPath": "BillOfMaterials.ComponentSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "BillOfMaterials.componentSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.1", + "figureNumber": 3, + "aasTemplate": "CIR4FUN.BillOfMaterials.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "billOfMaterials.componentSet[*]", + "elementId": "component", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#ComponentInformation", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Component", + "BillOfMaterials.ComponentSet.Component_00", + "http://www.w3id.org/pdto#ComponentInformation" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "Component", + "aasPath": "BillOfMaterials.ComponentSet.Component_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "BillOfMaterials.componentSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.1", + "figureNumber": 3, + "aasTemplate": "CIR4FUN.BillOfMaterials.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "component_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "billOfMaterials.componentSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "BillOfMaterials.ComponentSet.Component_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "BillOfMaterials.ComponentSet.Component_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "BillOfMaterials.componentSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.1", + "figureNumber": 3, + "aasTemplate": "CIR4FUN.BillOfMaterials.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "billOfMaterials.componentSet[*].mandatory", + "elementId": "mandatory", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#isMandatory", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Mandatory", + "BillOfMaterials.ComponentSet.Component_00.Mandatory", + "http://www.w3id.org/pdto#isMandatory" + ], + "source": { + "conflictNote": "D4.1 prose + ontology samples include; AAS+UML omit", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Mandatory", + "aasPath": "BillOfMaterials.ComponentSet.Component_00.Mandatory", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "BillOfMaterials.componentSet[].mandatory", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.1", + "figureNumber": 3, + "aasTemplate": "CIR4FUN.BillOfMaterials.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "D4.1 prose + ontology samples include; AAS+UML omit" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "billOfMaterials.componentSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "BillOfMaterials.ComponentSet.Component_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "Reference", + "aasPath": "BillOfMaterials.ComponentSet.Component_00.Reference", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "BillOfMaterials.componentSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.1", + "figureNumber": 3, + "aasTemplate": "CIR4FUN.BillOfMaterials.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "billOfMaterials.partSet", + "elementId": "partSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "PartSet", + "BillOfMaterials.PartSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "PartSet", + "aasPath": "BillOfMaterials.PartSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "BillOfMaterials.partSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.1", + "figureNumber": 3, + "aasTemplate": "CIR4FUN.BillOfMaterials.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "billOfMaterials.partSet[*]", + "elementId": "part", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#PartInformation", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Part", + "BillOfMaterials.PartSet.Part_00", + "http://www.w3id.org/pdto#PartInformation" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "Part", + "aasPath": "BillOfMaterials.PartSet.Part_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "BillOfMaterials.partSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.1", + "figureNumber": 3, + "aasTemplate": "CIR4FUN.BillOfMaterials.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "part_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "billOfMaterials.partSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "BillOfMaterials.PartSet.Part_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "BillOfMaterials.PartSet.Part_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "BillOfMaterials.partSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.1", + "figureNumber": 3, + "aasTemplate": "CIR4FUN.BillOfMaterials.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "billOfMaterials.partSet[*].mandatory", + "elementId": "mandatory", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#isMandatory", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Mandatory", + "BillOfMaterials.PartSet.Part_00.Mandatory", + "http://www.w3id.org/pdto#isMandatory" + ], + "source": { + "aasValueType": "xs:boolean", + "conflictNote": "AAS+UML include; D4.1 prose omits", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Mandatory", + "aasPath": "BillOfMaterials.PartSet.Part_00.Mandatory", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "BillOfMaterials.partSet[].mandatory", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.1", + "figureNumber": 3, + "aasTemplate": "CIR4FUN.BillOfMaterials.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "AAS+UML include; D4.1 prose omits" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "billOfMaterials.partSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "BillOfMaterials.PartSet.Part_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "Reference", + "aasPath": "BillOfMaterials.PartSet.Part_00.Reference", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "BillOfMaterials.partSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.1", + "figureNumber": 3, + "aasTemplate": "CIR4FUN.BillOfMaterials.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "carbonFootprint", + "elementId": "carbonFootprint", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#CarbonFootprint", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "CarbonFootprint", + "https://www.cir4fun.eu/ontology/dpp4fun#CarbonFootprint" + ], + "source": { + "aasModelType": "Submodel", + "d4Name": "CarbonFootprint", + "aasPath": "CarbonFootprint", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "EN root elementId uniqueness; D4.1 aspect presence not mandated" + }, + "javaConcept": "CarbonFootprint", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.2", + "figureNumber": 4, + "aasTemplate": "CIR4FUN.CarbonFootprint.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "carbonFootprint.carbonFootprintCalculationSet", + "elementId": "carbonFootprintCalculationSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "CarbonFootprintCalculationSet", + "CarbonFootprint.CarbonFootprintCalculationSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "CarbonFootprintCalculationSet", + "aasPath": "CarbonFootprint.CarbonFootprintCalculationSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "CarbonFootprint.carbonFootprintCalculationSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.2", + "figureNumber": 4, + "aasTemplate": "CIR4FUN.CarbonFootprint.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "carbonFootprint.carbonFootprintCalculationSet[*]", + "elementId": "carbonFootprintCalculation", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#CarbonFootprintCalculation", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "CarbonFootprintCalculation", + "CarbonFootprint.CarbonFootprintCalculationSet.CarbonFootprintCalculation_00", + "http://www.w3id.org/pdto#CarbonFootprintCalculation" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "CarbonFootprintCalculation", + "aasPath": "CarbonFootprint.CarbonFootprintCalculationSet.CarbonFootprintCalculation_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "CarbonFootprint.carbonFootprintCalculationSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.2", + "figureNumber": 4, + "aasTemplate": "CIR4FUN.CarbonFootprint.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "carbonFootprintCalculation_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "carbonFootprint.carbonFootprintCalculationSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "CarbonFootprint.CarbonFootprintCalculationSet.CarbonFootprintCalculation_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "Reference", + "aasPath": "CarbonFootprint.CarbonFootprintCalculationSet.CarbonFootprintCalculation_00.Reference", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "CarbonFootprint.carbonFootprintCalculationSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.2", + "figureNumber": 4, + "aasTemplate": "CIR4FUN.CarbonFootprint.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "carbonFootprint.carbonFootprintCalculationSet[*].co2eq", + "elementId": "co2eq", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "CO2eq", + "CarbonFootprint.CarbonFootprintCalculationSet.CarbonFootprintCalculation_00.CO2eq" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "CO2eq", + "aasPath": "CarbonFootprint.CarbonFootprintCalculationSet.CarbonFootprintCalculation_00.CO2eq", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "CarbonFootprint.carbonFootprintCalculationSet[].co2eq", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.2", + "figureNumber": 4, + "aasTemplate": "CIR4FUN.CarbonFootprint.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "carbonFootprint.carbonFootprintCalculationSet[*].phase", + "elementId": "phase", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Phase", + "CarbonFootprint.CarbonFootprintCalculationSet.CarbonFootprintCalculation_00.Phase" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Phase", + "aasPath": "CarbonFootprint.CarbonFootprintCalculationSet.CarbonFootprintCalculation_00.Phase", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "CarbonFootprint.carbonFootprintCalculationSet[].phase", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.2", + "figureNumber": 4, + "aasTemplate": "CIR4FUN.CarbonFootprint.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "carbonFootprint.carbonFootprintCalculationSet[*].quantityOfMeasurementForCalculation", + "elementId": "quantityOfMeasurementForCalculation", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "QuantityOfMeasurementForCalculation", + "CarbonFootprint.CarbonFootprintCalculationSet.CarbonFootprintCalculation_00.QuantityOfMeasurementForCalculation" + ], + "source": { + "aasValueType": "xs:integer", + "conflictNote": "D4.1 prose+UML String; AAS xs:integer", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "QuantityOfMeasurementForCalculation", + "aasPath": "CarbonFootprint.CarbonFootprintCalculationSet.CarbonFootprintCalculation_00.QuantityOfMeasurementForCalculation", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "CarbonFootprint.carbonFootprintCalculationSet[].quantityOfMeasurementForCalculation", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.2", + "figureNumber": 4, + "aasTemplate": "CIR4FUN.CarbonFootprint.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "D4.1 prose+UML String; AAS xs:integer" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "carbonFootprint.carbonFootprintCalculationSet[*].impactUnitForCalculation", + "elementId": "impactUnitForCalculation", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ImpactUnitForCalculation", + "CarbonFootprint.CarbonFootprintCalculationSet.CarbonFootprintCalculation_00.ImpactUnitForCalculation" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ImpactUnitForCalculation", + "aasPath": "CarbonFootprint.CarbonFootprintCalculationSet.CarbonFootprintCalculation_00.ImpactUnitForCalculation", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "CarbonFootprint.carbonFootprintCalculationSet[].impactUnitForCalculation", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.2", + "figureNumber": 4, + "aasTemplate": "CIR4FUN.CarbonFootprint.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "durability", + "elementId": "durability", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Durability", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Durability", + "https://www.cir4fun.eu/ontology/dpp4fun#Durability" + ], + "source": { + "aasModelType": "Submodel", + "d4Name": "Durability", + "aasPath": "Durability", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "EN root elementId uniqueness; D4.1 aspect presence not mandated" + }, + "javaConcept": "Durability", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.3", + "figureNumber": 5, + "aasTemplate": "CIR4FUN.Durability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "durability.cycleLife", + "elementId": "cycleLife", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "CycleLife", + "Durability.CycleLife" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "CycleLife", + "aasPath": "Durability.CycleLife", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Durability.cycleLife", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.3", + "figureNumber": 5, + "aasTemplate": "CIR4FUN.Durability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "durability.expectedLifetime", + "elementId": "expectedLifetime", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ExpectedLifetime", + "Durability.ExpectedLifetime" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "D4.1 prose calls this Expected Lifecycle; AAS+UML use ExpectedLifetime", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ExpectedLifetime", + "aasPath": "Durability.ExpectedLifetime", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Durability.expectedLifetime", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.3", + "figureNumber": 5, + "aasTemplate": "CIR4FUN.Durability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "D4.1 prose calls this Expected Lifecycle; AAS+UML use ExpectedLifetime" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "durability.certificateSet", + "elementId": "certificateSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "CertificateSet", + "Durability.CertificateSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "CertificateSet", + "aasPath": "Durability.CertificateSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "Durability.certificateSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.3", + "figureNumber": 5, + "aasTemplate": "CIR4FUN.Durability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "durability.certificateSet[*]", + "elementId": "certificate", + "objectType": "DataElementCollection", + "dictionaryReference": "http://w3id.org/dppo/ontology/dpp-info/CertificateInformation", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Certificate", + "Durability.CertificateSet.Certificate_00", + "http://w3id.org/dppo/ontology/dpp-info/CertificateInformation" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "Certificate", + "aasPath": "Durability.CertificateSet.Certificate_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "Durability.certificateSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.3", + "figureNumber": 5, + "aasTemplate": "CIR4FUN.Durability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "certificate_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "durability.certificateSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "Durability.CertificateSet.Certificate_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "Durability.CertificateSet.Certificate_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Durability.certificateSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.3", + "figureNumber": 5, + "aasTemplate": "CIR4FUN.Durability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_ASSESSMENT_NAME_REQUIRED" + }, + { + "path": "durability.certificateSet[*].issueDate", + "elementId": "issueDate", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasIssuedDate", + "valueDataType": "xsd:dateTime", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "IssueDate", + "Durability.CertificateSet.Certificate_00.IssueDate", + "http://www.w3id.org/pdto#hasIssuedDate" + ], + "source": { + "aasValueType": "xs:dateTime", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "IssueDate", + "aasPath": "Durability.CertificateSet.Certificate_00.IssueDate", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Durability.certificateSet[].issueDate", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.3", + "figureNumber": 5, + "aasTemplate": "CIR4FUN.Durability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "durability.certificateSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "Durability.CertificateSet.Certificate_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "Durability.CertificateSet.Certificate_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Durability.certificateSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.3", + "figureNumber": 5, + "aasTemplate": "CIR4FUN.Durability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_ASSESSMENT_DESCRIPTION_REQUIRED" + }, + { + "path": "durability.certificateSet[*].validityPolicy", + "elementId": "validityPolicy", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ValidityPolicy", + "Durability.CertificateSet.Certificate_00.ValidityPolicy" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ValidityPolicy", + "aasPath": "Durability.CertificateSet.Certificate_00.ValidityPolicy", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Durability.certificateSet[].validityPolicy", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.3", + "figureNumber": 5, + "aasTemplate": "CIR4FUN.Durability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "durability.certificateSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "Durability.CertificateSet.Certificate_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "Reference", + "aasPath": "Durability.CertificateSet.Certificate_00.Reference", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Durability.certificateSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.3", + "figureNumber": 5, + "aasTemplate": "CIR4FUN.Durability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "durability.standardSet", + "elementId": "standardSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "StandardSet", + "Durability.StandardSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "StandardSet", + "aasPath": "Durability.StandardSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "Durability.standardSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.3", + "figureNumber": 5, + "aasTemplate": "CIR4FUN.Durability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "durability.standardSet[*]", + "elementId": "standard", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#StandardInformation", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Standard", + "Durability.StandardSet.Standard_00", + "http://www.w3id.org/pdto#StandardInformation" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "Standard", + "aasPath": "Durability.StandardSet.Standard_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "Durability.standardSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.3", + "figureNumber": 5, + "aasTemplate": "CIR4FUN.Durability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "standard_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "durability.standardSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "Durability.StandardSet.Standard_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "Durability.StandardSet.Standard_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Durability.standardSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.3", + "figureNumber": 5, + "aasTemplate": "CIR4FUN.Durability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_ASSESSMENT_NAME_REQUIRED" + }, + { + "path": "durability.standardSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "Durability.StandardSet.Standard_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "Durability.StandardSet.Standard_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Durability.standardSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.3", + "figureNumber": 5, + "aasTemplate": "CIR4FUN.Durability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_ASSESSMENT_DESCRIPTION_REQUIRED" + }, + { + "path": "durability.standardSet[*].complianceStatus", + "elementId": "complianceStatus", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ComplianceStatus", + "Durability.StandardSet.Standard_00.ComplianceStatus" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ComplianceStatus", + "aasPath": "Durability.StandardSet.Standard_00.ComplianceStatus", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Durability.standardSet[].complianceStatus", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.3", + "figureNumber": 5, + "aasTemplate": "CIR4FUN.Durability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "durability.standardSet[*].status", + "elementId": "status", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Status", + "Durability.StandardSet.Standard_00.Status" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Status", + "aasPath": "Durability.StandardSet.Standard_00.Status", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Durability.standardSet[].status", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.3", + "figureNumber": 5, + "aasTemplate": "CIR4FUN.Durability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "durability.standardSet[*].version", + "elementId": "version", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Version", + "Durability.StandardSet.Standard_00.Version" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Version", + "aasPath": "Durability.StandardSet.Standard_00.Version", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Durability.standardSet[].version", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.3", + "figureNumber": 5, + "aasTemplate": "CIR4FUN.Durability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "durability.standardSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "Durability.StandardSet.Standard_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "Reference", + "aasPath": "Durability.StandardSet.Standard_00.Reference", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Durability.standardSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.3", + "figureNumber": 5, + "aasTemplate": "CIR4FUN.Durability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "durability.scoringSet", + "elementId": "scoringSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ScoringSet", + "Durability.ScoringSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "ScoringSet", + "aasPath": "Durability.ScoringSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "Durability.scoringSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.3", + "figureNumber": 5, + "aasTemplate": "CIR4FUN.Durability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "durability.scoringSet[*]", + "elementId": "scoring", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#ScoringInformation", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Scoring", + "Durability.ScoringSet.Scoring_00", + "http://www.w3id.org/pdto#ScoringInformation" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "Scoring", + "aasPath": "Durability.ScoringSet.Scoring_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "Durability.scoringSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.3", + "figureNumber": 5, + "aasTemplate": "CIR4FUN.Durability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "scoring_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "durability.scoringSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "Durability.ScoringSet.Scoring_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "Durability.ScoringSet.Scoring_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Durability.scoringSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.3", + "figureNumber": 5, + "aasTemplate": "CIR4FUN.Durability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_ASSESSMENT_NAME_REQUIRED" + }, + { + "path": "durability.scoringSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "Durability.ScoringSet.Scoring_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "Durability.ScoringSet.Scoring_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Durability.scoringSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.3", + "figureNumber": 5, + "aasTemplate": "CIR4FUN.Durability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_ASSESSMENT_DESCRIPTION_REQUIRED" + }, + { + "path": "durability.scoringSet[*].value", + "elementId": "value", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Value", + "Durability.ScoringSet.Scoring_00.Value" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Value", + "aasPath": "Durability.ScoringSet.Scoring_00.Value", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Durability.scoringSet[].value", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.3", + "figureNumber": 5, + "aasTemplate": "CIR4FUN.Durability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "durability.scoringSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "Durability.ScoringSet.Scoring_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "Reference", + "aasPath": "Durability.ScoringSet.Scoring_00.Reference", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Durability.scoringSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.3", + "figureNumber": 5, + "aasTemplate": "CIR4FUN.Durability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "environmentalImpact", + "elementId": "environmentalImpact", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#EnvironmentalImpact", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "EnvironmentalImpact", + "https://www.cir4fun.eu/ontology/dpp4fun#EnvironmentalImpact" + ], + "source": { + "aasModelType": "Submodel", + "d4Name": "EnvironmentalImpact", + "aasPath": "EnvironmentalImpact", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "EN root elementId uniqueness; D4.1 aspect presence not mandated" + }, + "javaConcept": "EnvironmentalImpact", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.4", + "figureNumber": 6, + "aasTemplate": "CIR4FUN.EnvironmentalImpact.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "environmentalImpact.indicatorSet", + "elementId": "indicatorSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "IndicatorSet", + "EnvironmentalImpact.IndicatorSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "IndicatorSet", + "aasPath": "EnvironmentalImpact.IndicatorSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "EnvironmentalImpact.indicatorSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.4", + "figureNumber": 6, + "aasTemplate": "CIR4FUN.EnvironmentalImpact.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "environmentalImpact.indicatorSet[*]", + "elementId": "indicator", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#IndicatorInformation", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Indicator", + "EnvironmentalImpact.IndicatorSet.Indicator_00", + "http://www.w3id.org/pdto#IndicatorInformation" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "Indicator", + "aasPath": "EnvironmentalImpact.IndicatorSet.Indicator_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "EnvironmentalImpact.indicatorSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.4", + "figureNumber": 6, + "aasTemplate": "CIR4FUN.EnvironmentalImpact.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "indicator_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "environmentalImpact.indicatorSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "EnvironmentalImpact.IndicatorSet.Indicator_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "EnvironmentalImpact.IndicatorSet.Indicator_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "EnvironmentalImpact.indicatorSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.4", + "figureNumber": 6, + "aasTemplate": "CIR4FUN.EnvironmentalImpact.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_ASSESSMENT_NAME_REQUIRED" + }, + { + "path": "environmentalImpact.indicatorSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "EnvironmentalImpact.IndicatorSet.Indicator_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "EnvironmentalImpact.IndicatorSet.Indicator_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "EnvironmentalImpact.indicatorSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.4", + "figureNumber": 6, + "aasTemplate": "CIR4FUN.EnvironmentalImpact.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_ASSESSMENT_DESCRIPTION_REQUIRED" + }, + { + "path": "environmentalImpact.indicatorSet[*].value", + "elementId": "value", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Value", + "EnvironmentalImpact.IndicatorSet.Indicator_00.Value" + ], + "source": { + "aasValueType": "xs:double", + "conflictNote": "D4.1 prose+UML String; AAS xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Value", + "aasPath": "EnvironmentalImpact.IndicatorSet.Indicator_00.Value", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "EnvironmentalImpact.indicatorSet[].value", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.4", + "figureNumber": 6, + "aasTemplate": "CIR4FUN.EnvironmentalImpact.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "D4.1 prose+UML String; AAS xs:double" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "environmentalImpact.indicatorSet[*].unit", + "elementId": "unit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Unit", + "EnvironmentalImpact.IndicatorSet.Indicator_00.Unit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Unit", + "aasPath": "EnvironmentalImpact.IndicatorSet.Indicator_00.Unit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "EnvironmentalImpact.indicatorSet[].unit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.4", + "figureNumber": 6, + "aasTemplate": "CIR4FUN.EnvironmentalImpact.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "environmentalImpact.indicatorSet[*].calculationMethod", + "elementId": "calculationMethod", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "CalculationMethod", + "EnvironmentalImpact.IndicatorSet.Indicator_00.CalculationMethod", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "CalculationMethod", + "aasPath": "EnvironmentalImpact.IndicatorSet.Indicator_00.CalculationMethod", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "EnvironmentalImpact.indicatorSet[].calculationMethod", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.4", + "figureNumber": 6, + "aasTemplate": "CIR4FUN.EnvironmentalImpact.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "healthImpact", + "elementId": "healthImpact", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#HealthImpact", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "HealthImpact", + "https://www.cir4fun.eu/ontology/dpp4fun#HealthImpact" + ], + "source": { + "aasModelType": "Submodel", + "d4Name": "HealthImpact", + "aasPath": "HealthImpact", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "EN root elementId uniqueness; D4.1 aspect presence not mandated" + }, + "javaConcept": "HealthImpact", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.5", + "figureNumber": 7, + "aasTemplate": "CIR4FUN.HealthImpact.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "healthImpact.indicatorSet", + "elementId": "indicatorSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "IndicatorSet", + "HealthImpact.IndicatorSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "IndicatorSet", + "aasPath": "HealthImpact.IndicatorSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "HealthImpact.indicatorSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.5", + "figureNumber": 7, + "aasTemplate": "CIR4FUN.HealthImpact.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "healthImpact.indicatorSet[*]", + "elementId": "indicator", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#IndicatorInformation", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Indicator", + "HealthImpact.IndicatorSet.Indicator_00", + "http://www.w3id.org/pdto#IndicatorInformation" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "Indicator", + "aasPath": "HealthImpact.IndicatorSet.Indicator_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "HealthImpact.indicatorSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.5", + "figureNumber": 7, + "aasTemplate": "CIR4FUN.HealthImpact.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "indicator_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "healthImpact.indicatorSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "HealthImpact.IndicatorSet.Indicator_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "HealthImpact.IndicatorSet.Indicator_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "HealthImpact.indicatorSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.5", + "figureNumber": 7, + "aasTemplate": "CIR4FUN.HealthImpact.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_ASSESSMENT_NAME_REQUIRED" + }, + { + "path": "healthImpact.indicatorSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "HealthImpact.IndicatorSet.Indicator_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "HealthImpact.IndicatorSet.Indicator_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "HealthImpact.indicatorSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.5", + "figureNumber": 7, + "aasTemplate": "CIR4FUN.HealthImpact.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_ASSESSMENT_DESCRIPTION_REQUIRED" + }, + { + "path": "healthImpact.indicatorSet[*].value", + "elementId": "value", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Value", + "HealthImpact.IndicatorSet.Indicator_00.Value" + ], + "source": { + "aasValueType": "xs:double", + "conflictNote": "D4.1 prose+UML String; AAS xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Value", + "aasPath": "HealthImpact.IndicatorSet.Indicator_00.Value", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "HealthImpact.indicatorSet[].value", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.5", + "figureNumber": 7, + "aasTemplate": "CIR4FUN.HealthImpact.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "D4.1 prose+UML String; AAS xs:double" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "healthImpact.indicatorSet[*].unit", + "elementId": "unit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Unit", + "HealthImpact.IndicatorSet.Indicator_00.Unit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Unit", + "aasPath": "HealthImpact.IndicatorSet.Indicator_00.Unit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "HealthImpact.indicatorSet[].unit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.5", + "figureNumber": 7, + "aasTemplate": "CIR4FUN.HealthImpact.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "healthImpact.indicatorSet[*].calculationMethod", + "elementId": "calculationMethod", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "CalculationMethod", + "HealthImpact.IndicatorSet.Indicator_00.CalculationMethod", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "CalculationMethod", + "aasPath": "HealthImpact.IndicatorSet.Indicator_00.CalculationMethod", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "HealthImpact.indicatorSet[].calculationMethod", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.5", + "figureNumber": 7, + "aasTemplate": "CIR4FUN.HealthImpact.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "healthImpact.indicatorSet[*].hazardType", + "elementId": "hazardType", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "HazardType", + "HealthImpact.IndicatorSet.Indicator_00.HazardType" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "HazardType", + "aasPath": "HealthImpact.IndicatorSet.Indicator_00.HazardType", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "HealthImpact.indicatorSet[].hazardType", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.5", + "figureNumber": 7, + "aasTemplate": "CIR4FUN.HealthImpact.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate", + "elementId": "nameplate", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#Nameplate", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Nameplate", + "http://www.w3id.org/pdto#Nameplate" + ], + "source": { + "aasModelType": "Submodel", + "d4Name": "Nameplate", + "aasPath": "Nameplate", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "EN root elementId uniqueness; D4.1 aspect presence not mandated" + }, + "javaConcept": "Nameplate", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.gtinCode", + "elementId": "gtinCode", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "GTINCode", + "Nameplate.GTINCode" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "GTINCode", + "aasPath": "Nameplate.GTINCode", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.gtinCode", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.internalArticleNumber", + "elementId": "internalArticleNumber", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "InternalArticleNumber", + "Nameplate.InternalArticleNumber" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "InternalArticleNumber", + "aasPath": "Nameplate.InternalArticleNumber", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.internalArticleNumber", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.batchNumber", + "elementId": "batchNumber", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "BatchNumber", + "Nameplate.BatchNumber" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "BatchNumber", + "aasPath": "Nameplate.BatchNumber", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.batchNumber", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.manufacturerName", + "elementId": "manufacturerName", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ManufacturerName", + "Nameplate.ManufacturerName" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ManufacturerName", + "aasPath": "Nameplate.ManufacturerName", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.manufacturerName", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.manufacturerProductDescription", + "elementId": "manufacturerProductDescription", + "objectType": "MultiLanguageDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ManufacturerProductDescription", + "Nameplate.ManufacturerProductDescription" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ManufacturerProductDescription", + "aasPath": "Nameplate.ManufacturerProductDescription", + "aasModelType": "MultiLanguageProperty", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.manufacturerProductDescription", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.manufacturerProductDesignation", + "elementId": "manufacturerProductDesignation", + "objectType": "MultiLanguageDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ManufacturerProductDesignation", + "Nameplate.ManufacturerProductDesignation" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ManufacturerProductDesignation", + "aasPath": "Nameplate.ManufacturerProductDesignation", + "aasModelType": "MultiLanguageProperty", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.manufacturerProductDesignation", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.manufacturerProductFamily", + "elementId": "manufacturerProductFamily", + "objectType": "MultiLanguageDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ManufacturerProductFamily", + "Nameplate.ManufacturerProductFamily" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ManufacturerProductFamily", + "aasPath": "Nameplate.ManufacturerProductFamily", + "aasModelType": "MultiLanguageProperty", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.manufacturerProductFamily", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.manufacturerProductRoot", + "elementId": "manufacturerProductRoot", + "objectType": "MultiLanguageDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ManufacturerProductRoot", + "Nameplate.ManufacturerProductRoot" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ManufacturerProductRoot", + "aasPath": "Nameplate.ManufacturerProductRoot", + "aasModelType": "MultiLanguageProperty", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.manufacturerProductRoot", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.manufacturerProductOrderSuffix", + "elementId": "manufacturerProductOrderSuffix", + "objectType": "MultiLanguageDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ManufacturerProductOrderSuffix", + "Nameplate.ManufacturerProductOrderSuffix" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ManufacturerProductOrderSuffix", + "aasPath": "Nameplate.ManufacturerProductOrderSuffix", + "aasModelType": "MultiLanguageProperty", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.manufacturerProductOrderSuffix", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.uriOfManufacturer", + "elementId": "uriOfManufacturer", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "URIOfManufacturer", + "Nameplate.URIOfManufacturer" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "URIOfManufacturer", + "aasPath": "Nameplate.URIOfManufacturer", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.uriOfManufacturer", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.glnOfManufacturer", + "elementId": "glnOfManufacturer", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "GLNOfManufacturer", + "Nameplate.GLNOfManufacturer" + ], + "source": { + "aasValueType": "xs:integer", + "conflictNote": "D4.1 says String; AAS xs:integer; identifiers must preserve lexical form", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "GLNOfManufacturer", + "aasPath": "Nameplate.GLNOfManufacturer", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.glnOfManufacturer", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "D4.1 says String; AAS xs:integer; identifiers must preserve lexical form" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.hsCodeOfTheWCO", + "elementId": "hsCodeOfTheWCO", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "HSCodeOfTheWCO", + "Nameplate.HSCodeOfTheWCO" + ], + "source": { + "aasValueType": "xs:integer", + "conflictNote": "D4.1 says String; AAS xs:integer", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "HSCodeOfTheWCO", + "aasPath": "Nameplate.HSCodeOfTheWCO", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.hsCodeOfTheWCO", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "D4.1 says String; AAS xs:integer" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.customsTariffNumber", + "elementId": "customsTariffNumber", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "CustomsTariffNumber", + "Nameplate.CustomsTariffNumber" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "CustomsTariffNumber", + "aasPath": "Nameplate.CustomsTariffNumber", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.customsTariffNumber", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.brand", + "elementId": "brand", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Brand", + "Nameplate.Brand" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Brand", + "aasPath": "Nameplate.Brand", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.brand", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.productType", + "elementId": "productType", + "objectType": "MultiLanguageDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ProductType", + "Nameplate.ProductType" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ProductType", + "aasPath": "Nameplate.ProductType", + "aasModelType": "MultiLanguageProperty", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.productType", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.uriOfTheProduct", + "elementId": "uriOfTheProduct", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "URIOfTheProduct", + "Nameplate.URIOfTheProduct" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "URIOfTheProduct", + "aasPath": "Nameplate.URIOfTheProduct", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.uriOfTheProduct", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.productArticleNumberOfSupplier", + "elementId": "productArticleNumberOfSupplier", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ProductArticleNumberOfSupplier", + "Nameplate.ProductArticleNumberOfSupplier" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ProductArticleNumberOfSupplier", + "aasPath": "Nameplate.ProductArticleNumberOfSupplier", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.productArticleNumberOfSupplier", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.nameOfSupplier", + "elementId": "nameOfSupplier", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "NameOfSupplier", + "Nameplate.NameOfSupplier" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "NameOfSupplier", + "aasPath": "Nameplate.NameOfSupplier", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.nameOfSupplier", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.supplierProductDescription", + "elementId": "supplierProductDescription", + "objectType": "MultiLanguageDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "SupplierProductDescription", + "Nameplate.SupplierProductDescription" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "SupplierProductDescription", + "aasPath": "Nameplate.SupplierProductDescription", + "aasModelType": "MultiLanguageProperty", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.supplierProductDescription", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.supplierProductDesignation", + "elementId": "supplierProductDesignation", + "objectType": "MultiLanguageDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "SupplierProductDesignation", + "Nameplate.SupplierProductDesignation" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "SupplierProductDesignation", + "aasPath": "Nameplate.SupplierProductDesignation", + "aasModelType": "MultiLanguageProperty", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.supplierProductDesignation", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.supplierProductFamily", + "elementId": "supplierProductFamily", + "objectType": "MultiLanguageDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "SupplierProductFamily", + "Nameplate.SupplierProductFamily" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "SupplierProductFamily", + "aasPath": "Nameplate.SupplierProductFamily", + "aasModelType": "MultiLanguageProperty", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.supplierProductFamily", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.supplierProductRoot", + "elementId": "supplierProductRoot", + "objectType": "MultiLanguageDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "SupplierProductRoot", + "Nameplate.SupplierProductRoot" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "SupplierProductRoot", + "aasPath": "Nameplate.SupplierProductRoot", + "aasModelType": "MultiLanguageProperty", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.supplierProductRoot", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.supplierProductOrderSuffix", + "elementId": "supplierProductOrderSuffix", + "objectType": "MultiLanguageDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "SupplierProductOrderSuffix", + "Nameplate.SupplierProductOrderSuffix" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "SupplierProductOrderSuffix", + "aasPath": "Nameplate.SupplierProductOrderSuffix", + "aasModelType": "MultiLanguageProperty", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.supplierProductOrderSuffix", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.supplierProductType", + "elementId": "supplierProductType", + "objectType": "MultiLanguageDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "SupplierProductType", + "Nameplate.SupplierProductType" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "SupplierProductType", + "aasPath": "Nameplate.SupplierProductType", + "aasModelType": "MultiLanguageProperty", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.supplierProductType", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.uriOfSupplier", + "elementId": "uriOfSupplier", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "URIOfSupplier", + "Nameplate.URIOfSupplier" + ], + "source": { + "conflictNote": "D4.1 prose includes URI of Supplier; pinned AAS omits", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "URIOfSupplier", + "aasPath": "Nameplate.URIOfSupplier", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.uriOfSupplier", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "D4.1 prose includes URI of Supplier; pinned AAS omits" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.glnOfSupplier", + "elementId": "glnOfSupplier", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "GLNOfSupplier", + "Nameplate.GLNOfSupplier" + ], + "source": { + "aasValueType": "xs:integer", + "conflictNote": "D4.1 says String; AAS xs:integer", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "GLNOfSupplier", + "aasPath": "Nameplate.GLNOfSupplier", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.glnOfSupplier", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "D4.1 says String; AAS xs:integer" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.addressOfAdditionalLink", + "elementId": "addressOfAdditionalLink", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "AddressOfAdditionalLink", + "Nameplate.AddressOfAdditionalLink" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "AddressOfAdditionalLink", + "aasPath": "Nameplate.AddressOfAdditionalLink", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.addressOfAdditionalLink", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.contact", + "elementId": "contact", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#ContactInformation", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Contact", + "Nameplate.Contact", + "http://www.w3id.org/pdto#ContactInformation" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "Contact", + "aasPath": "Nameplate.Contact", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.contact", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.contact.organization", + "elementId": "organization", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Organization", + "Nameplate.Contact.Organization" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Organization", + "aasPath": "Nameplate.Contact.Organization", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.contact.organization", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.contact.address", + "elementId": "address", + "objectType": "DataElementCollection", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Address", + "Nameplate.Contact.Address" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "Address", + "aasPath": "Nameplate.Contact.Address", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.contact.address", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.contact.address.country", + "elementId": "country", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Country", + "Nameplate.Contact.Address.Country" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Country", + "aasPath": "Nameplate.Contact.Address.Country", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.contact.address.country", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.contact.address.zipCode", + "elementId": "zipCode", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ZipCode", + "Nameplate.Contact.Address.ZipCode" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ZipCode", + "aasPath": "Nameplate.Contact.Address.ZipCode", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.contact.address.zipCode", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.contact.address.region", + "elementId": "region", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Region", + "Nameplate.Contact.Address.Region" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Region", + "aasPath": "Nameplate.Contact.Address.Region", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.contact.address.region", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.contact.address.town", + "elementId": "town", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Town", + "Nameplate.Contact.Address.Town" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Town", + "aasPath": "Nameplate.Contact.Address.Town", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.contact.address.town", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.contact.address.street", + "elementId": "street", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Street", + "Nameplate.Contact.Address.Street" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Street", + "aasPath": "Nameplate.Contact.Address.Street", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.contact.address.street", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.contact.telephone", + "elementId": "telephone", + "objectType": "DataElementCollection", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Telephone", + "Nameplate.Contact.Telephone" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "Telephone", + "aasPath": "Nameplate.Contact.Telephone", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.contact.telephone", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.contact.telephone.telephoneNumber", + "elementId": "telephoneNumber", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "TelephoneNumber", + "Nameplate.Contact.Telephone.TelephoneNumber" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "TelephoneNumber", + "aasPath": "Nameplate.Contact.Telephone.TelephoneNumber", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.contact.telephone.telephoneNumber", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.contact.telephone.typeOfTelephone", + "elementId": "typeOfTelephone", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "TypeOfTelephone", + "Nameplate.Contact.Telephone.TypeOfTelephone" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "TypeOfTelephone", + "aasPath": "Nameplate.Contact.Telephone.TypeOfTelephone", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.contact.telephone.typeOfTelephone", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.contact.email", + "elementId": "email", + "objectType": "DataElementCollection", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Email", + "Nameplate.Contact.Email" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "Email", + "aasPath": "Nameplate.Contact.Email", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.contact.email", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.contact.email.emailAddress", + "elementId": "emailAddress", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "EmailAddress", + "Nameplate.Contact.Email.EmailAddress" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "EmailAddress", + "aasPath": "Nameplate.Contact.Email.EmailAddress", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.contact.email.emailAddress", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "nameplate.contact.email.typeOfEmail", + "elementId": "typeOfEmail", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "TypeOfEmail", + "Nameplate.Contact.Email.TypeOfEmail" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "TypeOfEmail", + "aasPath": "Nameplate.Contact.Email.TypeOfEmail", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Nameplate.contact.email.typeOfEmail", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.6", + "figureNumber": 8, + "aasTemplate": "CIR4FUN.Nameplate.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "productRelationship", + "elementId": "productRelationship", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#ProductRelationship", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ProductRelationship", + "https://www.cir4fun.eu/ontology/dpp4fun#ProductRelationship" + ], + "source": { + "aasModelType": "Submodel", + "d4Name": "ProductRelationship", + "aasPath": "ProductRelationship", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "EN root elementId uniqueness; D4.1 aspect presence not mandated" + }, + "javaConcept": "ProductRelationship", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.7", + "figureNumber": 9, + "aasTemplate": "CIR4FUN.ProductRelationship.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "productRelationship.accessorySet", + "elementId": "accessorySet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "AccessorySet", + "ProductRelationship.AccessorySet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "AccessorySet", + "aasPath": "ProductRelationship.AccessorySet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "ProductRelationship.accessorySet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.7", + "figureNumber": 9, + "aasTemplate": "CIR4FUN.ProductRelationship.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "productRelationship.accessorySet[*]", + "elementId": "accessory", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Accessory", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Accessory", + "ProductRelationship.AccessorySet.Accessory_00", + "https://www.cir4fun.eu/ontology/dpp4fun#Accessory" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "Accessory", + "aasPath": "ProductRelationship.AccessorySet.Accessory_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "ProductRelationship.accessorySet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.7", + "figureNumber": 9, + "aasTemplate": "CIR4FUN.ProductRelationship.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "accessory_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "productRelationship.accessorySet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "ProductRelationship.AccessorySet.Accessory_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "ProductRelationship.AccessorySet.Accessory_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ProductRelationship.accessorySet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.7", + "figureNumber": 9, + "aasTemplate": "CIR4FUN.ProductRelationship.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "productRelationship.accessorySet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "ProductRelationship.AccessorySet.Accessory_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "ProductRelationship.AccessorySet.Accessory_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ProductRelationship.accessorySet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.7", + "figureNumber": 9, + "aasTemplate": "CIR4FUN.ProductRelationship.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "productRelationship.accessorySet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "ProductRelationship.AccessorySet.Accessory_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "Reference", + "aasPath": "ProductRelationship.AccessorySet.Accessory_00.Reference", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ProductRelationship.accessorySet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.7", + "figureNumber": 9, + "aasTemplate": "CIR4FUN.ProductRelationship.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "productRelationship.collectionMemberSet", + "elementId": "collectionMemberSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "CollectionMemberSet", + "ProductRelationship.CollectionMemberSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "CollectionMemberSet", + "aasPath": "ProductRelationship.CollectionMemberSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "ProductRelationship.collectionMemberSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.7", + "figureNumber": 9, + "aasTemplate": "CIR4FUN.ProductRelationship.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "productRelationship.collectionMemberSet[*]", + "elementId": "collectionMember", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#CollectionMember", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "CollectionMember", + "ProductRelationship.CollectionMemberSet.CollectionMember_00", + "https://www.cir4fun.eu/ontology/dpp4fun#CollectionMember" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "CollectionMember", + "aasPath": "ProductRelationship.CollectionMemberSet.CollectionMember_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "ProductRelationship.collectionMemberSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.7", + "figureNumber": 9, + "aasTemplate": "CIR4FUN.ProductRelationship.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "collectionMember_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "productRelationship.collectionMemberSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "ProductRelationship.CollectionMemberSet.CollectionMember_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "ProductRelationship.CollectionMemberSet.CollectionMember_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ProductRelationship.collectionMemberSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.7", + "figureNumber": 9, + "aasTemplate": "CIR4FUN.ProductRelationship.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "productRelationship.collectionMemberSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "ProductRelationship.CollectionMemberSet.CollectionMember_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "ProductRelationship.CollectionMemberSet.CollectionMember_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ProductRelationship.collectionMemberSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.7", + "figureNumber": 9, + "aasTemplate": "CIR4FUN.ProductRelationship.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "productRelationship.collectionMemberSet[*].collection", + "elementId": "collection", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Collection", + "ProductRelationship.CollectionMemberSet.CollectionMember_00.Collection" + ], + "source": { + "conflictNote": "D4.1 prose+UML include; AAS omits", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Collection", + "aasPath": "ProductRelationship.CollectionMemberSet.CollectionMember_00.Collection", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ProductRelationship.collectionMemberSet[].collection", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.7", + "figureNumber": 9, + "aasTemplate": "CIR4FUN.ProductRelationship.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "D4.1 prose+UML include; AAS omits" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "productRelationship.collectionMemberSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "ProductRelationship.CollectionMemberSet.CollectionMember_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "Reference", + "aasPath": "ProductRelationship.CollectionMemberSet.CollectionMember_00.Reference", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ProductRelationship.collectionMemberSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.7", + "figureNumber": 9, + "aasTemplate": "CIR4FUN.ProductRelationship.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "recyclability", + "elementId": "recyclability", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Recyclability", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Recyclability", + "https://www.cir4fun.eu/ontology/dpp4fun#Recyclability" + ], + "source": { + "aasModelType": "Submodel", + "d4Name": "Recyclability", + "aasPath": "Recyclability", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "EN root elementId uniqueness; D4.1 aspect presence not mandated" + }, + "javaConcept": "Recyclability", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.8", + "figureNumber": 10, + "aasTemplate": "CIR4FUN.Recyclability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "recyclability.recycledContent", + "elementId": "recycledContent", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "RecycledContent", + "Recyclability.RecycledContent" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "D4.1 prose+UML Double; AAS xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "RecycledContent", + "aasPath": "Recyclability.RecycledContent", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Recyclability.recycledContent", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.8", + "figureNumber": 10, + "aasTemplate": "CIR4FUN.Recyclability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "D4.1 prose+UML Double; AAS xs:string" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "recyclability.recyclable", + "elementId": "recyclable", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Recyclable", + "Recyclability.Recyclable" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Recyclable", + "aasPath": "Recyclability.Recyclable", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Recyclability.recyclable", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.8", + "figureNumber": 10, + "aasTemplate": "CIR4FUN.Recyclability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "recyclability.recycledMaterialIncluded", + "elementId": "recycledMaterialIncluded", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "RecycledMaterialIncluded", + "Recyclability.RecycledMaterialIncluded" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "RecycledMaterialIncluded", + "aasPath": "Recyclability.RecycledMaterialIncluded", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Recyclability.recycledMaterialIncluded", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.8", + "figureNumber": 10, + "aasTemplate": "CIR4FUN.Recyclability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "recyclability.certificateSet", + "elementId": "certificateSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "CertificateSet", + "Recyclability.CertificateSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "CertificateSet", + "aasPath": "Recyclability.CertificateSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "Recyclability.certificateSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.8", + "figureNumber": 10, + "aasTemplate": "CIR4FUN.Recyclability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "recyclability.certificateSet[*]", + "elementId": "certificate", + "objectType": "DataElementCollection", + "dictionaryReference": "http://w3id.org/dppo/ontology/dpp-info/CertificateInformation", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Certificate", + "Recyclability.CertificateSet.Certificate_00", + "http://w3id.org/dppo/ontology/dpp-info/CertificateInformation" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "Certificate", + "aasPath": "Recyclability.CertificateSet.Certificate_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "Recyclability.certificateSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.8", + "figureNumber": 10, + "aasTemplate": "CIR4FUN.Recyclability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "certificate_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "recyclability.certificateSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "Recyclability.CertificateSet.Certificate_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "Recyclability.CertificateSet.Certificate_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Recyclability.certificateSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.8", + "figureNumber": 10, + "aasTemplate": "CIR4FUN.Recyclability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_ASSESSMENT_NAME_REQUIRED" + }, + { + "path": "recyclability.certificateSet[*].issueDate", + "elementId": "issueDate", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasIssuedDate", + "valueDataType": "xsd:dateTime", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "IssueDate", + "Recyclability.CertificateSet.Certificate_00.IssueDate", + "http://www.w3id.org/pdto#hasIssuedDate" + ], + "source": { + "aasValueType": "xs:dateTime", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "IssueDate", + "aasPath": "Recyclability.CertificateSet.Certificate_00.IssueDate", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Recyclability.certificateSet[].issueDate", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.8", + "figureNumber": 10, + "aasTemplate": "CIR4FUN.Recyclability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "recyclability.certificateSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "Recyclability.CertificateSet.Certificate_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "Recyclability.CertificateSet.Certificate_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Recyclability.certificateSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.8", + "figureNumber": 10, + "aasTemplate": "CIR4FUN.Recyclability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_ASSESSMENT_DESCRIPTION_REQUIRED" + }, + { + "path": "recyclability.certificateSet[*].validityPolicy", + "elementId": "validityPolicy", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ValidityPolicy", + "Recyclability.CertificateSet.Certificate_00.ValidityPolicy" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ValidityPolicy", + "aasPath": "Recyclability.CertificateSet.Certificate_00.ValidityPolicy", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Recyclability.certificateSet[].validityPolicy", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.8", + "figureNumber": 10, + "aasTemplate": "CIR4FUN.Recyclability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "recyclability.certificateSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "Recyclability.CertificateSet.Certificate_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "Reference", + "aasPath": "Recyclability.CertificateSet.Certificate_00.Reference", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Recyclability.certificateSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.8", + "figureNumber": 10, + "aasTemplate": "CIR4FUN.Recyclability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "recyclability.standardSet", + "elementId": "standardSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "StandardSet", + "Recyclability.StandardSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "StandardSet", + "aasPath": "Recyclability.StandardSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "Recyclability.standardSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.8", + "figureNumber": 10, + "aasTemplate": "CIR4FUN.Recyclability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "recyclability.standardSet[*]", + "elementId": "standard", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#StandardInformation", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Standard", + "Recyclability.StandardSet.Standard_00", + "http://www.w3id.org/pdto#StandardInformation" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "Standard", + "aasPath": "Recyclability.StandardSet.Standard_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "Recyclability.standardSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.8", + "figureNumber": 10, + "aasTemplate": "CIR4FUN.Recyclability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "standard_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "recyclability.standardSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "Recyclability.StandardSet.Standard_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "Recyclability.StandardSet.Standard_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Recyclability.standardSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.8", + "figureNumber": 10, + "aasTemplate": "CIR4FUN.Recyclability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_ASSESSMENT_NAME_REQUIRED" + }, + { + "path": "recyclability.standardSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "Recyclability.StandardSet.Standard_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "Recyclability.StandardSet.Standard_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Recyclability.standardSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.8", + "figureNumber": 10, + "aasTemplate": "CIR4FUN.Recyclability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_ASSESSMENT_DESCRIPTION_REQUIRED" + }, + { + "path": "recyclability.standardSet[*].complianceStatus", + "elementId": "complianceStatus", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ComplianceStatus", + "Recyclability.StandardSet.Standard_00.ComplianceStatus" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ComplianceStatus", + "aasPath": "Recyclability.StandardSet.Standard_00.ComplianceStatus", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Recyclability.standardSet[].complianceStatus", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.8", + "figureNumber": 10, + "aasTemplate": "CIR4FUN.Recyclability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "recyclability.standardSet[*].status", + "elementId": "status", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Status", + "Recyclability.StandardSet.Standard_00.Status" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Status", + "aasPath": "Recyclability.StandardSet.Standard_00.Status", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Recyclability.standardSet[].status", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.8", + "figureNumber": 10, + "aasTemplate": "CIR4FUN.Recyclability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "recyclability.standardSet[*].version", + "elementId": "version", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Version", + "Recyclability.StandardSet.Standard_00.Version" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Version", + "aasPath": "Recyclability.StandardSet.Standard_00.Version", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Recyclability.standardSet[].version", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.8", + "figureNumber": 10, + "aasTemplate": "CIR4FUN.Recyclability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "recyclability.standardSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "Recyclability.StandardSet.Standard_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "Reference", + "aasPath": "Recyclability.StandardSet.Standard_00.Reference", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Recyclability.standardSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.8", + "figureNumber": 10, + "aasTemplate": "CIR4FUN.Recyclability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "recyclability.recyclingInstructionSet", + "elementId": "recyclingInstructionSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "RecyclingInstructionSet", + "Recyclability.RecyclingInstructionSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "RecyclingInstructionSet", + "aasPath": "Recyclability.RecyclingInstructionSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "Recyclability.recyclingInstructionSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.8", + "figureNumber": 10, + "aasTemplate": "CIR4FUN.Recyclability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "recyclability.recyclingInstructionSet[*]", + "elementId": "recyclingInstruction", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#RecyclingInstruction", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "RecyclingInstruction", + "Recyclability.RecyclingInstructionSet.RecyclingInstruction_00", + "http://www.w3id.org/pdto#RecyclingInstruction" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "RecyclingInstruction", + "aasPath": "Recyclability.RecyclingInstructionSet.RecyclingInstruction_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "Recyclability.recyclingInstructionSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.8", + "figureNumber": 10, + "aasTemplate": "CIR4FUN.Recyclability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "recyclingInstruction_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "recyclability.recyclingInstructionSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "Recyclability.RecyclingInstructionSet.RecyclingInstruction_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "Recyclability.RecyclingInstructionSet.RecyclingInstruction_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Recyclability.recyclingInstructionSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.8", + "figureNumber": 10, + "aasTemplate": "CIR4FUN.Recyclability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_NAME_REQUIRED" + }, + { + "path": "recyclability.recyclingInstructionSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "Recyclability.RecyclingInstructionSet.RecyclingInstruction_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "Recyclability.RecyclingInstructionSet.RecyclingInstruction_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Recyclability.recyclingInstructionSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.8", + "figureNumber": 10, + "aasTemplate": "CIR4FUN.Recyclability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_DESCRIPTION_REQUIRED" + }, + { + "path": "recyclability.recyclingInstructionSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "Recyclability.RecyclingInstructionSet.RecyclingInstruction_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "Reference", + "aasPath": "Recyclability.RecyclingInstructionSet.RecyclingInstruction_00.Reference", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Recyclability.recyclingInstructionSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.8", + "figureNumber": 10, + "aasTemplate": "CIR4FUN.Recyclability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_REFERENCE_REQUIRED" + }, + { + "path": "refurbishment", + "elementId": "refurbishment", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Refurbishment", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Refurbishment", + "https://www.cir4fun.eu/ontology/dpp4fun#Refurbishment" + ], + "source": { + "aasModelType": "Submodel", + "d4Name": "Refurbishment", + "aasPath": "Refurbishment", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "EN root elementId uniqueness; D4.1 aspect presence not mandated" + }, + "javaConcept": "Refurbishment", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.9", + "figureNumber": 11, + "aasTemplate": "CIR4FUN.Refurbishment.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "refurbishment.refurbishmentInstructionSet", + "elementId": "refurbishmentInstructionSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "RefurbishmentInstructionSet", + "Refurbishment.RefurbishmentInstructionSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "RefurbishmentInstructionSet", + "aasPath": "Refurbishment.RefurbishmentInstructionSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "Refurbishment.refurbishmentInstructionSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.9", + "figureNumber": 11, + "aasTemplate": "CIR4FUN.Refurbishment.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "refurbishment.refurbishmentInstructionSet[*]", + "elementId": "refurbishmentInstruction", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#RefurbishmentInstruction", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "RefurbishmentInstruction", + "Refurbishment.RefurbishmentInstructionSet.RefurbishmentInstruction_00", + "http://www.w3id.org/pdto#RefurbishmentInstruction" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "RefurbishmentInstruction", + "aasPath": "Refurbishment.RefurbishmentInstructionSet.RefurbishmentInstruction_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "Refurbishment.refurbishmentInstructionSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.9", + "figureNumber": 11, + "aasTemplate": "CIR4FUN.Refurbishment.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "refurbishmentInstruction_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "refurbishment.refurbishmentInstructionSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "Refurbishment.RefurbishmentInstructionSet.RefurbishmentInstruction_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "Refurbishment.RefurbishmentInstructionSet.RefurbishmentInstruction_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Refurbishment.refurbishmentInstructionSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.9", + "figureNumber": 11, + "aasTemplate": "CIR4FUN.Refurbishment.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_NAME_REQUIRED" + }, + { + "path": "refurbishment.refurbishmentInstructionSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "Refurbishment.RefurbishmentInstructionSet.RefurbishmentInstruction_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "Refurbishment.RefurbishmentInstructionSet.RefurbishmentInstruction_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Refurbishment.refurbishmentInstructionSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.9", + "figureNumber": 11, + "aasTemplate": "CIR4FUN.Refurbishment.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_DESCRIPTION_REQUIRED" + }, + { + "path": "refurbishment.refurbishmentInstructionSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "Refurbishment.RefurbishmentInstructionSet.RefurbishmentInstruction_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "Reference", + "aasPath": "Refurbishment.RefurbishmentInstructionSet.RefurbishmentInstruction_00.Reference", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Refurbishment.refurbishmentInstructionSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.9", + "figureNumber": 11, + "aasTemplate": "CIR4FUN.Refurbishment.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_REFERENCE_REQUIRED" + }, + { + "path": "reliability", + "elementId": "reliability", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Reliability", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reliability", + "https://www.cir4fun.eu/ontology/dpp4fun#Reliability" + ], + "source": { + "aasModelType": "Submodel", + "d4Name": "Reliability", + "aasPath": "Reliability", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "EN root elementId uniqueness; D4.1 aspect presence not mandated" + }, + "javaConcept": "Reliability", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.10", + "figureNumber": 12, + "aasTemplate": "CIR4FUN.Reliability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "reliability.certificateSet", + "elementId": "certificateSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "CertificateSet", + "Reliability.CertificateSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "CertificateSet", + "aasPath": "Reliability.CertificateSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "Reliability.certificateSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.10", + "figureNumber": 12, + "aasTemplate": "CIR4FUN.Reliability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "reliability.certificateSet[*]", + "elementId": "certificate", + "objectType": "DataElementCollection", + "dictionaryReference": "http://w3id.org/dppo/ontology/dpp-info/CertificateInformation", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Certificate", + "Reliability.CertificateSet.Certificate_00", + "http://w3id.org/dppo/ontology/dpp-info/CertificateInformation" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "Certificate", + "aasPath": "Reliability.CertificateSet.Certificate_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "Reliability.certificateSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.10", + "figureNumber": 12, + "aasTemplate": "CIR4FUN.Reliability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "certificate_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "reliability.certificateSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "Reliability.CertificateSet.Certificate_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "Reliability.CertificateSet.Certificate_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Reliability.certificateSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.10", + "figureNumber": 12, + "aasTemplate": "CIR4FUN.Reliability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_ASSESSMENT_NAME_REQUIRED" + }, + { + "path": "reliability.certificateSet[*].issueDate", + "elementId": "issueDate", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasIssuedDate", + "valueDataType": "xsd:dateTime", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "IssueDate", + "Reliability.CertificateSet.Certificate_00.IssueDate", + "http://www.w3id.org/pdto#hasIssuedDate" + ], + "source": { + "aasValueType": "xs:dateTime", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "IssueDate", + "aasPath": "Reliability.CertificateSet.Certificate_00.IssueDate", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Reliability.certificateSet[].issueDate", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.10", + "figureNumber": 12, + "aasTemplate": "CIR4FUN.Reliability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "reliability.certificateSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "Reliability.CertificateSet.Certificate_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "Reliability.CertificateSet.Certificate_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Reliability.certificateSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.10", + "figureNumber": 12, + "aasTemplate": "CIR4FUN.Reliability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_ASSESSMENT_DESCRIPTION_REQUIRED" + }, + { + "path": "reliability.certificateSet[*].validityPolicy", + "elementId": "validityPolicy", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ValidityPolicy", + "Reliability.CertificateSet.Certificate_00.ValidityPolicy" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ValidityPolicy", + "aasPath": "Reliability.CertificateSet.Certificate_00.ValidityPolicy", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Reliability.certificateSet[].validityPolicy", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.10", + "figureNumber": 12, + "aasTemplate": "CIR4FUN.Reliability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "reliability.certificateSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "Reliability.CertificateSet.Certificate_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "Reference", + "aasPath": "Reliability.CertificateSet.Certificate_00.Reference", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Reliability.certificateSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.10", + "figureNumber": 12, + "aasTemplate": "CIR4FUN.Reliability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "reliability.standardSet", + "elementId": "standardSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "StandardSet", + "Reliability.StandardSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "StandardSet", + "aasPath": "Reliability.StandardSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "Reliability.standardSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.10", + "figureNumber": 12, + "aasTemplate": "CIR4FUN.Reliability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "reliability.standardSet[*]", + "elementId": "standard", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#StandardInformation", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Standard", + "Reliability.StandardSet.Standard_00", + "http://www.w3id.org/pdto#StandardInformation" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "Standard", + "aasPath": "Reliability.StandardSet.Standard_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "Reliability.standardSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.10", + "figureNumber": 12, + "aasTemplate": "CIR4FUN.Reliability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "standard_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "reliability.standardSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "Reliability.StandardSet.Standard_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "Reliability.StandardSet.Standard_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Reliability.standardSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.10", + "figureNumber": 12, + "aasTemplate": "CIR4FUN.Reliability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_ASSESSMENT_NAME_REQUIRED" + }, + { + "path": "reliability.standardSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "Reliability.StandardSet.Standard_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "Reliability.StandardSet.Standard_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Reliability.standardSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.10", + "figureNumber": 12, + "aasTemplate": "CIR4FUN.Reliability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_ASSESSMENT_DESCRIPTION_REQUIRED" + }, + { + "path": "reliability.standardSet[*].complianceStatus", + "elementId": "complianceStatus", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ComplianceStatus", + "Reliability.StandardSet.Standard_00.ComplianceStatus" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ComplianceStatus", + "aasPath": "Reliability.StandardSet.Standard_00.ComplianceStatus", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Reliability.standardSet[].complianceStatus", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.10", + "figureNumber": 12, + "aasTemplate": "CIR4FUN.Reliability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "reliability.standardSet[*].status", + "elementId": "status", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Status", + "Reliability.StandardSet.Standard_00.Status" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Status", + "aasPath": "Reliability.StandardSet.Standard_00.Status", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Reliability.standardSet[].status", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.10", + "figureNumber": 12, + "aasTemplate": "CIR4FUN.Reliability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "reliability.standardSet[*].version", + "elementId": "version", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Version", + "Reliability.StandardSet.Standard_00.Version" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Version", + "aasPath": "Reliability.StandardSet.Standard_00.Version", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Reliability.standardSet[].version", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.10", + "figureNumber": 12, + "aasTemplate": "CIR4FUN.Reliability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "reliability.standardSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "Reliability.StandardSet.Standard_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "Reference", + "aasPath": "Reliability.StandardSet.Standard_00.Reference", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Reliability.standardSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.10", + "figureNumber": 12, + "aasTemplate": "CIR4FUN.Reliability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "reliability.scoringSet", + "elementId": "scoringSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ScoringSet", + "Reliability.ScoringSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "ScoringSet", + "aasPath": "Reliability.ScoringSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "Reliability.scoringSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.10", + "figureNumber": 12, + "aasTemplate": "CIR4FUN.Reliability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "reliability.scoringSet[*]", + "elementId": "scoring", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#ScoringInformation", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Scoring", + "Reliability.ScoringSet.Scoring_00", + "http://www.w3id.org/pdto#ScoringInformation" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "Scoring", + "aasPath": "Reliability.ScoringSet.Scoring_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "Reliability.scoringSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.10", + "figureNumber": 12, + "aasTemplate": "CIR4FUN.Reliability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "scoring_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "reliability.scoringSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "Reliability.ScoringSet.Scoring_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "Reliability.ScoringSet.Scoring_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Reliability.scoringSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.10", + "figureNumber": 12, + "aasTemplate": "CIR4FUN.Reliability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_ASSESSMENT_NAME_REQUIRED" + }, + { + "path": "reliability.scoringSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "Reliability.ScoringSet.Scoring_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "Reliability.ScoringSet.Scoring_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Reliability.scoringSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.10", + "figureNumber": 12, + "aasTemplate": "CIR4FUN.Reliability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_ASSESSMENT_DESCRIPTION_REQUIRED" + }, + { + "path": "reliability.scoringSet[*].value", + "elementId": "value", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Value", + "Reliability.ScoringSet.Scoring_00.Value" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Value", + "aasPath": "Reliability.ScoringSet.Scoring_00.Value", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Reliability.scoringSet[].value", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.10", + "figureNumber": 12, + "aasTemplate": "CIR4FUN.Reliability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "reliability.scoringSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "Reliability.ScoringSet.Scoring_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "Reference", + "aasPath": "Reliability.ScoringSet.Scoring_00.Reference", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Reliability.scoringSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.10", + "figureNumber": 12, + "aasTemplate": "CIR4FUN.Reliability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "remanufacturing", + "elementId": "remanufacturing", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Remanufacturing", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Remanufacturing", + "https://www.cir4fun.eu/ontology/dpp4fun#Remanufacturing" + ], + "source": { + "aasModelType": "Submodel", + "d4Name": "Remanufacturing", + "aasPath": "Remanufacturing", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "EN root elementId uniqueness; D4.1 aspect presence not mandated" + }, + "javaConcept": "Remanufacturing", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.11", + "figureNumber": 13, + "aasTemplate": "CIR4FUN.Remanufacturing.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "remanufacturing.assemblyInstructionSet", + "elementId": "assemblyInstructionSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "AssemblyInstructionSet", + "Remanufacturing.AssemblyInstructionSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "AssemblyInstructionSet", + "aasPath": "Remanufacturing.AssemblyInstructionSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "Remanufacturing.assemblyInstructionSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.11", + "figureNumber": 13, + "aasTemplate": "CIR4FUN.Remanufacturing.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "remanufacturing.assemblyInstructionSet[*]", + "elementId": "assemblyInstruction", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#AssemblyInstruction", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "AssemblyInstruction", + "Remanufacturing.AssemblyInstructionSet.AssemblyInstruction_00", + "http://www.w3id.org/pdto#AssemblyInstruction" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "AssemblyInstruction", + "aasPath": "Remanufacturing.AssemblyInstructionSet.AssemblyInstruction_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "Remanufacturing.assemblyInstructionSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.11", + "figureNumber": 13, + "aasTemplate": "CIR4FUN.Remanufacturing.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "assemblyInstruction_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "remanufacturing.assemblyInstructionSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "Remanufacturing.AssemblyInstructionSet.AssemblyInstruction_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "Remanufacturing.AssemblyInstructionSet.AssemblyInstruction_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Remanufacturing.assemblyInstructionSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.11", + "figureNumber": 13, + "aasTemplate": "CIR4FUN.Remanufacturing.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_NAME_REQUIRED" + }, + { + "path": "remanufacturing.assemblyInstructionSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "Remanufacturing.AssemblyInstructionSet.AssemblyInstruction_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "Remanufacturing.AssemblyInstructionSet.AssemblyInstruction_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Remanufacturing.assemblyInstructionSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.11", + "figureNumber": 13, + "aasTemplate": "CIR4FUN.Remanufacturing.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_DESCRIPTION_REQUIRED" + }, + { + "path": "remanufacturing.assemblyInstructionSet[*].targetAudience", + "elementId": "targetAudience", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "TargetAudience", + "Remanufacturing.AssemblyInstructionSet.AssemblyInstruction_00.TargetAudience" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "TargetAudience", + "aasPath": "Remanufacturing.AssemblyInstructionSet.AssemblyInstruction_00.TargetAudience", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Remanufacturing.assemblyInstructionSet[].targetAudience", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.11", + "figureNumber": 13, + "aasTemplate": "CIR4FUN.Remanufacturing.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "remanufacturing.assemblyInstructionSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "Remanufacturing.AssemblyInstructionSet.AssemblyInstruction_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "Reference", + "aasPath": "Remanufacturing.AssemblyInstructionSet.AssemblyInstruction_00.Reference", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Remanufacturing.assemblyInstructionSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.11", + "figureNumber": 13, + "aasTemplate": "CIR4FUN.Remanufacturing.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_REFERENCE_REQUIRED" + }, + { + "path": "remanufacturing.disassemblyInstructionSet", + "elementId": "disassemblyInstructionSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "DisassemblyInstructionSet", + "Remanufacturing.DisassemblyInstructionSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "DisassemblyInstructionSet", + "aasPath": "Remanufacturing.DisassemblyInstructionSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "Remanufacturing.disassemblyInstructionSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.11", + "figureNumber": 13, + "aasTemplate": "CIR4FUN.Remanufacturing.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "remanufacturing.disassemblyInstructionSet[*]", + "elementId": "disassemblyInstruction", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#DisassemblyInstruction", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "DisassemblyInstruction", + "Remanufacturing.DisassemblyInstructionSet.DiasssemblyInstruction_00", + "http://www.w3id.org/pdto#DisassemblyInstruction" + ], + "source": { + "conflictNote": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "DisassemblyInstruction", + "aasPath": "Remanufacturing.DisassemblyInstructionSet.DiasssemblyInstruction_00", + "aasModelType": "SubmodelElementCollection", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "Remanufacturing.disassemblyInstructionSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.11", + "figureNumber": 13, + "aasTemplate": "CIR4FUN.Remanufacturing.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "disassemblyInstruction_%02d" + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "remanufacturing.disassemblyInstructionSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "Remanufacturing.DisassemblyInstructionSet.DiasssemblyInstruction_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "Remanufacturing.DisassemblyInstructionSet.DiasssemblyInstruction_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Remanufacturing.disassemblyInstructionSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.11", + "figureNumber": 13, + "aasTemplate": "CIR4FUN.Remanufacturing.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction" + }, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_NAME_REQUIRED" + }, + { + "path": "remanufacturing.disassemblyInstructionSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "Remanufacturing.DisassemblyInstructionSet.DiasssemblyInstruction_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "Remanufacturing.DisassemblyInstructionSet.DiasssemblyInstruction_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Remanufacturing.disassemblyInstructionSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.11", + "figureNumber": 13, + "aasTemplate": "CIR4FUN.Remanufacturing.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction" + }, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_DESCRIPTION_REQUIRED" + }, + { + "path": "remanufacturing.disassemblyInstructionSet[*].targetAudience", + "elementId": "targetAudience", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "TargetAudience", + "Remanufacturing.DisassemblyInstructionSet.DiasssemblyInstruction_00.TargetAudience" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "TargetAudience", + "aasPath": "Remanufacturing.DisassemblyInstructionSet.DiasssemblyInstruction_00.TargetAudience", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Remanufacturing.disassemblyInstructionSet[].targetAudience", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.11", + "figureNumber": 13, + "aasTemplate": "CIR4FUN.Remanufacturing.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "remanufacturing.disassemblyInstructionSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "Remanufacturing.DisassemblyInstructionSet.DiasssemblyInstruction_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "conflictNote": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Reference", + "aasPath": "Remanufacturing.DisassemblyInstructionSet.DiasssemblyInstruction_00.Reference", + "aasModelType": "ReferenceElement", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Remanufacturing.disassemblyInstructionSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.11", + "figureNumber": 13, + "aasTemplate": "CIR4FUN.Remanufacturing.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction" + }, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_REFERENCE_REQUIRED" + }, + { + "path": "repairability", + "elementId": "repairability", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Repairability", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Repairability", + "https://www.cir4fun.eu/ontology/dpp4fun#Repairability" + ], + "source": { + "aasModelType": "Submodel", + "d4Name": "Repairability", + "aasPath": "Repairability", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "EN root elementId uniqueness; D4.1 aspect presence not mandated" + }, + "javaConcept": "Repairability", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.12", + "figureNumber": 14, + "aasTemplate": "CIR4FUN.Repairability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "repairability.sparePartsAvailable", + "elementId": "sparePartsAvailable", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "SparePartsAvailable", + "Repairability.SparePartsAvailable" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "SparePartsAvailable", + "aasPath": "Repairability.SparePartsAvailable", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Repairability.sparePartsAvailable", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.12", + "figureNumber": 14, + "aasTemplate": "CIR4FUN.Repairability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "repairability.repairInstructionSet", + "elementId": "repairInstructionSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "RepairInstructionSet", + "Repairability.RepairInstructionSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "RepairInstructionSet", + "aasPath": "Repairability.RepairInstructionSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "Repairability.repairInstructionSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.12", + "figureNumber": 14, + "aasTemplate": "CIR4FUN.Repairability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "repairability.repairInstructionSet[*]", + "elementId": "repairInstruction", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#RepairInstruction", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "RepairInstruction", + "Repairability.RepairInstructionSet.RepairInstruction_00", + "http://www.w3id.org/pdto#RepairInstruction" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "RepairInstruction", + "aasPath": "Repairability.RepairInstructionSet.RepairInstruction_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "Repairability.repairInstructionSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.12", + "figureNumber": 14, + "aasTemplate": "CIR4FUN.Repairability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "repairInstruction_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "repairability.repairInstructionSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "Repairability.RepairInstructionSet.RepairInstruction_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "Repairability.RepairInstructionSet.RepairInstruction_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Repairability.repairInstructionSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.12", + "figureNumber": 14, + "aasTemplate": "CIR4FUN.Repairability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_NAME_REQUIRED" + }, + { + "path": "repairability.repairInstructionSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "Repairability.RepairInstructionSet.RepairInstruction_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "Repairability.RepairInstructionSet.RepairInstruction_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Repairability.repairInstructionSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.12", + "figureNumber": 14, + "aasTemplate": "CIR4FUN.Repairability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_DESCRIPTION_REQUIRED" + }, + { + "path": "repairability.repairInstructionSet[*].targetAudience", + "elementId": "targetAudience", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "TargetAudience", + "Repairability.RepairInstructionSet.RepairInstruction_00.TargetAudience" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "TargetAudience", + "aasPath": "Repairability.RepairInstructionSet.RepairInstruction_00.TargetAudience", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Repairability.repairInstructionSet[].targetAudience", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.12", + "figureNumber": 14, + "aasTemplate": "CIR4FUN.Repairability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "repairability.repairInstructionSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "Repairability.RepairInstructionSet.RepairInstruction_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "Reference", + "aasPath": "Repairability.RepairInstructionSet.RepairInstruction_00.Reference", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Repairability.repairInstructionSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.12", + "figureNumber": 14, + "aasTemplate": "CIR4FUN.Repairability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_REFERENCE_REQUIRED" + }, + { + "path": "repairability.repairRecordSet", + "elementId": "repairRecordSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "RepairRecordSet", + "Repairability.RepairRecordSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "RepairRecordSet", + "aasPath": "Repairability.RepairRecordSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "Repairability.repairRecordSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.12", + "figureNumber": 14, + "aasTemplate": "CIR4FUN.Repairability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "repairability.repairRecordSet[*]", + "elementId": "repairRecord", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#RepairRecord", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "RepairRecord", + "Repairability.RepairRecordSet.RepairRecord_00", + "http://www.w3id.org/pdto#RepairRecord" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "RepairRecord", + "aasPath": "Repairability.RepairRecordSet.RepairRecord_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "Repairability.repairRecordSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.12", + "figureNumber": 14, + "aasTemplate": "CIR4FUN.Repairability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "repairRecord_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "repairability.repairRecordSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "Repairability.RepairRecordSet.RepairRecord_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "Repairability.RepairRecordSet.RepairRecord_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Repairability.repairRecordSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.12", + "figureNumber": 14, + "aasTemplate": "CIR4FUN.Repairability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_RECORD_NAME_REQUIRED" + }, + { + "path": "repairability.repairRecordSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "Repairability.RepairRecordSet.RepairRecord_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "Repairability.RepairRecordSet.RepairRecord_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Repairability.repairRecordSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.12", + "figureNumber": 14, + "aasTemplate": "CIR4FUN.Repairability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_RECORD_DESCRIPTION_REQUIRED" + }, + { + "path": "repairability.repairRecordSet[*].operator", + "elementId": "operator", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Operator", + "Repairability.RepairRecordSet.RepairRecord_00.Operator" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Operator", + "aasPath": "Repairability.RepairRecordSet.RepairRecord_00.Operator", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Repairability.repairRecordSet[].operator", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.12", + "figureNumber": 14, + "aasTemplate": "CIR4FUN.Repairability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_RECORD_OPERATOR_REQUIRED" + }, + { + "path": "repairability.repairRecordSet[*].time", + "elementId": "time", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Time", + "Repairability.RepairRecordSet.RepairRecord_00.Time" + ], + "source": { + "aasValueType": "xs:dateTime", + "conflictNote": "D4.1 prose+UML String; AAS xs:dateTime", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Time", + "aasPath": "Repairability.RepairRecordSet.RepairRecord_00.Time", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Repairability.repairRecordSet[].time", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.12", + "figureNumber": 14, + "aasTemplate": "CIR4FUN.Repairability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "D4.1 prose+UML String; AAS xs:dateTime" + }, + "explicitRequiredness": "DPP4FUN_RECORD_TIME_REQUIRED" + }, + { + "path": "repairability.repairRecordSet[*].location", + "elementId": "location", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Location", + "Repairability.RepairRecordSet.RepairRecord_00.Location" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Location", + "aasPath": "Repairability.RepairRecordSet.RepairRecord_00.Location", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Repairability.repairRecordSet[].location", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.12", + "figureNumber": 14, + "aasTemplate": "CIR4FUN.Repairability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "repairability.repairRecordSet[*].type", + "elementId": "type", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Type", + "Repairability.RepairRecordSet.RepairRecord_00.Type" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Type", + "aasPath": "Repairability.RepairRecordSet.RepairRecord_00.Type", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Repairability.repairRecordSet[].type", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.12", + "figureNumber": 14, + "aasTemplate": "CIR4FUN.Repairability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "reusability", + "elementId": "reusability", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Reusability", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reusability", + "https://www.cir4fun.eu/ontology/dpp4fun#Reusability" + ], + "source": { + "aasModelType": "Submodel", + "d4Name": "Reusability", + "aasPath": "Reusability", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "EN root elementId uniqueness; D4.1 aspect presence not mandated" + }, + "javaConcept": "Reusability", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.13", + "figureNumber": 15, + "aasTemplate": "CIR4FUN.Reusability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "reusability.secondHand", + "elementId": "secondHand", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "SecondHand", + "Reusability.SecondHand" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "D4.1 prose+UML Boolean; AAS xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "SecondHand", + "aasPath": "Reusability.SecondHand", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Reusability.secondHand", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.13", + "figureNumber": 15, + "aasTemplate": "CIR4FUN.Reusability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "D4.1 prose+UML Boolean; AAS xs:string" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "reusability.scoringSet", + "elementId": "scoringSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ScoringSet", + "Reusability.ScoringSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "ScoringSet", + "aasPath": "Reusability.ScoringSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "Reusability.scoringSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.13", + "figureNumber": 15, + "aasTemplate": "CIR4FUN.Reusability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "reusability.scoringSet[*]", + "elementId": "scoring", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#ScoringInformation", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Scoring", + "Reusability.ScoringSet.Scoring_00", + "http://www.w3id.org/pdto#ScoringInformation" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "Scoring", + "aasPath": "Reusability.ScoringSet.Scoring_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "Reusability.scoringSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.13", + "figureNumber": 15, + "aasTemplate": "CIR4FUN.Reusability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "scoring_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "reusability.scoringSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "Reusability.ScoringSet.Scoring_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "Reusability.ScoringSet.Scoring_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Reusability.scoringSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.13", + "figureNumber": 15, + "aasTemplate": "CIR4FUN.Reusability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_ASSESSMENT_NAME_REQUIRED" + }, + { + "path": "reusability.scoringSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "Reusability.ScoringSet.Scoring_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "Reusability.ScoringSet.Scoring_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Reusability.scoringSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.13", + "figureNumber": 15, + "aasTemplate": "CIR4FUN.Reusability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_ASSESSMENT_DESCRIPTION_REQUIRED" + }, + { + "path": "reusability.scoringSet[*].value", + "elementId": "value", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Value", + "Reusability.ScoringSet.Scoring_00.Value" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Value", + "aasPath": "Reusability.ScoringSet.Scoring_00.Value", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Reusability.scoringSet[].value", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.13", + "figureNumber": 15, + "aasTemplate": "CIR4FUN.Reusability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "reusability.scoringSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "Reusability.ScoringSet.Scoring_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "Reference", + "aasPath": "Reusability.ScoringSet.Scoring_00.Reference", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Reusability.scoringSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.13", + "figureNumber": 15, + "aasTemplate": "CIR4FUN.Reusability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "reusability.scoringInstructionSet", + "elementId": "scoringInstructionSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ScoringInstructionSet", + "Reusability.ScoringInstructionSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "ScoringInstructionSet", + "aasPath": "Reusability.ScoringInstructionSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "Reusability.scoringInstructionSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.13", + "figureNumber": 15, + "aasTemplate": "CIR4FUN.Reusability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "reusability.scoringInstructionSet[*]", + "elementId": "scoringInstruction", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#ScoringInstruction", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ScoringInstruction", + "Reusability.ScoringInstructionSet.ScoringInstruction_00", + "http://www.w3id.org/pdto#ScoringInstruction" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "ScoringInstruction", + "aasPath": "Reusability.ScoringInstructionSet.ScoringInstruction_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "Reusability.scoringInstructionSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.13", + "figureNumber": 15, + "aasTemplate": "CIR4FUN.Reusability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "scoringInstruction_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "reusability.scoringInstructionSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "Reusability.ScoringInstructionSet.ScoringInstruction_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "Reusability.ScoringInstructionSet.ScoringInstruction_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Reusability.scoringInstructionSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.13", + "figureNumber": 15, + "aasTemplate": "CIR4FUN.Reusability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_NAME_REQUIRED" + }, + { + "path": "reusability.scoringInstructionSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "Reusability.ScoringInstructionSet.ScoringInstruction_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "Reusability.ScoringInstructionSet.ScoringInstruction_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Reusability.scoringInstructionSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.13", + "figureNumber": 15, + "aasTemplate": "CIR4FUN.Reusability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_DESCRIPTION_REQUIRED" + }, + { + "path": "reusability.scoringInstructionSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "Reusability.ScoringInstructionSet.ScoringInstruction_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "Reference", + "aasPath": "Reusability.ScoringInstructionSet.ScoringInstruction_00.Reference", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Reusability.scoringInstructionSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.13", + "figureNumber": 15, + "aasTemplate": "CIR4FUN.Reusability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_REFERENCE_REQUIRED" + }, + { + "path": "rightOfEndUsers", + "elementId": "rightOfEndUsers", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#RightOfEndUsers", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "RightOfEndUsers", + "https://www.cir4fun.eu/ontology/dpp4fun#RightOfEndUsers" + ], + "source": { + "aasModelType": "Submodel", + "d4Name": "RightOfEndUsers", + "aasPath": "RightOfEndUsers", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "EN root elementId uniqueness; D4.1 aspect presence not mandated" + }, + "javaConcept": "RightOfEndUsers", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.14", + "figureNumber": 16, + "aasTemplate": "CIR4FUN.RightOfEndUsers.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "rightOfEndUsers.postSaleServiceSet", + "elementId": "postSaleServiceSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "PostSaleServiceSet", + "RightOfEndUsers.PostSaleServiceSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "PostSaleServiceSet", + "aasPath": "RightOfEndUsers.PostSaleServiceSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "RightOfEndUsers.postSaleServiceSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.14", + "figureNumber": 16, + "aasTemplate": "CIR4FUN.RightOfEndUsers.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "rightOfEndUsers.postSaleServiceSet[*]", + "elementId": "postSaleService", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Service", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "PostSaleService", + "RightOfEndUsers.PostSaleServiceSet.PostSaleService_00", + "https://www.cir4fun.eu/ontology/dpp4fun#Service" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "PostSaleService", + "aasPath": "RightOfEndUsers.PostSaleServiceSet.PostSaleService_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "RightOfEndUsers.postSaleServiceSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.14", + "figureNumber": 16, + "aasTemplate": "CIR4FUN.RightOfEndUsers.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "postSaleService_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "rightOfEndUsers.postSaleServiceSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "RightOfEndUsers.PostSaleServiceSet.PostSaleService_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "RightOfEndUsers.PostSaleServiceSet.PostSaleService_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "RightOfEndUsers.postSaleServiceSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.14", + "figureNumber": 16, + "aasTemplate": "CIR4FUN.RightOfEndUsers.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "rightOfEndUsers.postSaleServiceSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "RightOfEndUsers.PostSaleServiceSet.PostSaleService_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "RightOfEndUsers.PostSaleServiceSet.PostSaleService_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "RightOfEndUsers.postSaleServiceSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.14", + "figureNumber": 16, + "aasTemplate": "CIR4FUN.RightOfEndUsers.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "rightOfEndUsers.postSaleServiceSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "RightOfEndUsers.PostSaleServiceSet.PostSaleService_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "Reference", + "aasPath": "RightOfEndUsers.PostSaleServiceSet.PostSaleService_00.Reference", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "RightOfEndUsers.postSaleServiceSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.14", + "figureNumber": 16, + "aasTemplate": "CIR4FUN.RightOfEndUsers.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "rightOfEndUsers.policies", + "elementId": "policies", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Policies", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Policies", + "RightOfEndUsers.Policies", + "https://www.cir4fun.eu/ontology/dpp4fun#Policies" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "Policies", + "aasPath": "RightOfEndUsers.Policies", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "RightOfEndUsers.policies", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.14", + "figureNumber": 16, + "aasTemplate": "CIR4FUN.RightOfEndUsers.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "rightOfEndUsers.policies.prohibitionSet", + "elementId": "prohibitionSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ProhibitionSet", + "RightOfEndUsers.Policies.ProhibitionSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "ProhibitionSet", + "aasPath": "RightOfEndUsers.Policies.ProhibitionSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "RightOfEndUsers.policies.prohibitionSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.14", + "figureNumber": 16, + "aasTemplate": "CIR4FUN.RightOfEndUsers.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "rightOfEndUsers.policies.prohibitionSet[*]", + "elementId": "prohibition", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Prohibition", + "RightOfEndUsers.Policies.ProhibitionSet.Prohibition_00" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Prohibition", + "aasPath": "RightOfEndUsers.Policies.ProhibitionSet.Prohibition_00", + "aasModelType": "Property", + "cardinalityProvenance": "D4.1/AAS list item" + }, + "javaConcept": "RightOfEndUsers.policies.prohibitionSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.14", + "figureNumber": 16, + "aasTemplate": "CIR4FUN.RightOfEndUsers.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "prohibition_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "rightOfEndUsers.policies.permissionSet", + "elementId": "permissionSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "PermissionSet", + "RightOfEndUsers.Policies.PermissionSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "PermissionSet", + "aasPath": "RightOfEndUsers.Policies.PermissionSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "RightOfEndUsers.policies.permissionSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.14", + "figureNumber": 16, + "aasTemplate": "CIR4FUN.RightOfEndUsers.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "rightOfEndUsers.policies.permissionSet[*]", + "elementId": "permission", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Permission", + "RightOfEndUsers.Policies.PermissionSet.Permission_00" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Permission", + "aasPath": "RightOfEndUsers.Policies.PermissionSet.Permission_00", + "aasModelType": "Property", + "cardinalityProvenance": "D4.1/AAS list item" + }, + "javaConcept": "RightOfEndUsers.policies.permissionSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.14", + "figureNumber": 16, + "aasTemplate": "CIR4FUN.RightOfEndUsers.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "permission_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "safety", + "elementId": "safety", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Safety", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Safety", + "https://www.cir4fun.eu/ontology/dpp4fun#Safety" + ], + "source": { + "aasModelType": "Submodel", + "d4Name": "Safety", + "aasPath": "Safety", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "EN root elementId uniqueness; D4.1 aspect presence not mandated" + }, + "javaConcept": "Safety", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.15", + "figureNumber": 17, + "aasTemplate": "CIR4FUN.Safety.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "safety.ageRestriction", + "elementId": "ageRestriction", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "AgeRestriction", + "Safety.AgeRestriction" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "AgeRestriction", + "aasPath": "Safety.AgeRestriction", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Safety.ageRestriction", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.15", + "figureNumber": 17, + "aasTemplate": "CIR4FUN.Safety.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "safety.professionalUseOnly", + "elementId": "professionalUseOnly", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ProfessionalUseOnly", + "Safety.ProfessionalUseOnly" + ], + "source": { + "aasValueType": "xs:boolean", + "conflictNote": "D4.1 prose says String; UML+AAS Boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ProfessionalUseOnly", + "aasPath": "Safety.ProfessionalUseOnly", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Safety.professionalUseOnly", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.15", + "figureNumber": 17, + "aasTemplate": "CIR4FUN.Safety.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "D4.1 prose says String; UML+AAS Boolean" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "safety.certificateSet", + "elementId": "certificateSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "CertificateSet", + "Safety.CertificateSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "CertificateSet", + "aasPath": "Safety.CertificateSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "Safety.certificateSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.15", + "figureNumber": 17, + "aasTemplate": "CIR4FUN.Safety.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "safety.certificateSet[*]", + "elementId": "certificate", + "objectType": "DataElementCollection", + "dictionaryReference": "http://w3id.org/dppo/ontology/dpp-info/CertificateInformation", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Certificate", + "Safety.CertificateSet.Certificate_00", + "http://w3id.org/dppo/ontology/dpp-info/CertificateInformation" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "Certificate", + "aasPath": "Safety.CertificateSet.Certificate_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "Safety.certificateSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.15", + "figureNumber": 17, + "aasTemplate": "CIR4FUN.Safety.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "certificate_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "safety.certificateSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "Safety.CertificateSet.Certificate_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "Safety.CertificateSet.Certificate_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Safety.certificateSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.15", + "figureNumber": 17, + "aasTemplate": "CIR4FUN.Safety.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_ASSESSMENT_NAME_REQUIRED" + }, + { + "path": "safety.certificateSet[*].issueDate", + "elementId": "issueDate", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasIssuedDate", + "valueDataType": "xsd:dateTime", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "IssueDate", + "Safety.CertificateSet.Certificate_00.IssueDate", + "http://www.w3id.org/pdto#hasIssuedDate" + ], + "source": { + "aasValueType": "xs:dateTime", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "IssueDate", + "aasPath": "Safety.CertificateSet.Certificate_00.IssueDate", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Safety.certificateSet[].issueDate", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.15", + "figureNumber": 17, + "aasTemplate": "CIR4FUN.Safety.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "safety.certificateSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "Safety.CertificateSet.Certificate_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "Safety.CertificateSet.Certificate_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Safety.certificateSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.15", + "figureNumber": 17, + "aasTemplate": "CIR4FUN.Safety.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_ASSESSMENT_DESCRIPTION_REQUIRED" + }, + { + "path": "safety.certificateSet[*].validityPolicy", + "elementId": "validityPolicy", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ValidityPolicy", + "Safety.CertificateSet.Certificate_00.ValidityPolicy" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ValidityPolicy", + "aasPath": "Safety.CertificateSet.Certificate_00.ValidityPolicy", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Safety.certificateSet[].validityPolicy", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.15", + "figureNumber": 17, + "aasTemplate": "CIR4FUN.Safety.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "safety.certificateSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "Safety.CertificateSet.Certificate_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "Reference", + "aasPath": "Safety.CertificateSet.Certificate_00.Reference", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Safety.certificateSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.15", + "figureNumber": 17, + "aasTemplate": "CIR4FUN.Safety.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "safety.standardSet", + "elementId": "standardSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "StandardSet", + "Safety.StandardSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "StandardSet", + "aasPath": "Safety.StandardSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "Safety.standardSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.15", + "figureNumber": 17, + "aasTemplate": "CIR4FUN.Safety.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "safety.standardSet[*]", + "elementId": "standard", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#StandardInformation", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Standard", + "Safety.StandardSet.Standard_00", + "http://www.w3id.org/pdto#StandardInformation" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "Standard", + "aasPath": "Safety.StandardSet.Standard_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "Safety.standardSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.15", + "figureNumber": 17, + "aasTemplate": "CIR4FUN.Safety.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "standard_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "safety.standardSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "Safety.StandardSet.Standard_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "Safety.StandardSet.Standard_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Safety.standardSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.15", + "figureNumber": 17, + "aasTemplate": "CIR4FUN.Safety.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_ASSESSMENT_NAME_REQUIRED" + }, + { + "path": "safety.standardSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "Safety.StandardSet.Standard_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "Safety.StandardSet.Standard_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Safety.standardSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.15", + "figureNumber": 17, + "aasTemplate": "CIR4FUN.Safety.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_ASSESSMENT_DESCRIPTION_REQUIRED" + }, + { + "path": "safety.standardSet[*].complianceStatus", + "elementId": "complianceStatus", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ComplianceStatus", + "Safety.StandardSet.Standard_00.ComplianceStatus" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ComplianceStatus", + "aasPath": "Safety.StandardSet.Standard_00.ComplianceStatus", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Safety.standardSet[].complianceStatus", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.15", + "figureNumber": 17, + "aasTemplate": "CIR4FUN.Safety.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "safety.standardSet[*].status", + "elementId": "status", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Status", + "Safety.StandardSet.Standard_00.Status" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Status", + "aasPath": "Safety.StandardSet.Standard_00.Status", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Safety.standardSet[].status", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.15", + "figureNumber": 17, + "aasTemplate": "CIR4FUN.Safety.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "safety.standardSet[*].version", + "elementId": "version", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Version", + "Safety.StandardSet.Standard_00.Version" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Version", + "aasPath": "Safety.StandardSet.Standard_00.Version", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Safety.standardSet[].version", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.15", + "figureNumber": 17, + "aasTemplate": "CIR4FUN.Safety.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "safety.standardSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "Safety.StandardSet.Standard_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "Reference", + "aasPath": "Safety.StandardSet.Standard_00.Reference", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Safety.standardSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.15", + "figureNumber": 17, + "aasTemplate": "CIR4FUN.Safety.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "traceability", + "elementId": "traceability", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#Traceability", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Traceability", + "http://www.w3id.org/pdto#Traceability" + ], + "source": { + "aasModelType": "Submodel", + "d4Name": "Traceability", + "aasPath": "Traceability", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "EN root elementId uniqueness; D4.1 aspect presence not mandated" + }, + "javaConcept": "Traceability", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "traceability.currentPhase", + "elementId": "currentPhase", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "CurrentPhase", + "Traceability.CurrentPhase" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "CurrentPhase", + "aasPath": "Traceability.CurrentPhase", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Traceability.currentPhase", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "traceability.location", + "elementId": "location", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Location", + "Traceability.Location" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Location", + "aasPath": "Traceability.Location", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Traceability.location", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "traceability.designRecordSet", + "elementId": "designRecordSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "DesignRecordSet", + "Traceability.DesignRecordSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "DesignRecordSet", + "aasPath": "Traceability.DesignRecordSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "Traceability.designRecordSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "traceability.designRecordSet[*]", + "elementId": "designRecord", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#DesignRecord", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "DesignRecord", + "Traceability.DesignRecordSet.DesignRecord_00", + "http://www.w3id.org/pdto#DesignRecord" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "DesignRecord", + "aasPath": "Traceability.DesignRecordSet.DesignRecord_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "Traceability.designRecordSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "designRecord_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "traceability.designRecordSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "Traceability.DesignRecordSet.DesignRecord_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "Traceability.DesignRecordSet.DesignRecord_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Traceability.designRecordSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_RECORD_NAME_REQUIRED" + }, + { + "path": "traceability.designRecordSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "Traceability.DesignRecordSet.DesignRecord_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "Traceability.DesignRecordSet.DesignRecord_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Traceability.designRecordSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_RECORD_DESCRIPTION_REQUIRED" + }, + { + "path": "traceability.designRecordSet[*].operator", + "elementId": "operator", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Operator", + "Traceability.DesignRecordSet.DesignRecord_00.Operator" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Operator", + "aasPath": "Traceability.DesignRecordSet.DesignRecord_00.Operator", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Traceability.designRecordSet[].operator", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_RECORD_OPERATOR_REQUIRED" + }, + { + "path": "traceability.designRecordSet[*].time", + "elementId": "time", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:dateTime", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Time", + "Traceability.DesignRecordSet.DesignRecord_00.Time" + ], + "source": { + "aasValueType": "xs:dateTime", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Time", + "aasPath": "Traceability.DesignRecordSet.DesignRecord_00.Time", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Traceability.designRecordSet[].time", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_RECORD_TIME_REQUIRED" + }, + { + "path": "traceability.designRecordSet[*].version", + "elementId": "version", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Version", + "Traceability.DesignRecordSet.DesignRecord_00.Version" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Version", + "aasPath": "Traceability.DesignRecordSet.DesignRecord_00.Version", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Traceability.designRecordSet[].version", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "traceability.designRecordSet[*].status", + "elementId": "status", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Status", + "Traceability.DesignRecordSet.DesignRecord_00.Status" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Status", + "aasPath": "Traceability.DesignRecordSet.DesignRecord_00.Status", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Traceability.designRecordSet[].status", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_RECORD_STATUS_REQUIRED" + }, + { + "path": "traceability.designRecordSet[*].designFile", + "elementId": "designFile", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "DesignFile", + "Traceability.DesignRecordSet.DesignRecord_00.DesignFile", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "DesignFile", + "aasPath": "Traceability.DesignRecordSet.DesignRecord_00.DesignFile", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Traceability.designRecordSet[].designFile", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "traceability.manufacturingRecordSet", + "elementId": "manufacturingRecordSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ManufacturingRecordSet", + "Traceability.ManufacturingRecordSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "ManufacturingRecordSet", + "aasPath": "Traceability.ManufacturingRecordSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "Traceability.manufacturingRecordSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "traceability.manufacturingRecordSet[*]", + "elementId": "manufacturingRecord", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#ManufacturingRecord", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ManufacturingRecord", + "Traceability.ManufacturingRecordSet.ManufacturingStateRecord_00", + "http://www.w3id.org/pdto#ManufacturingRecord" + ], + "source": { + "conflictNote": "Pinned AAS idShort is ManufacturingStateRecord_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ManufacturingRecord", + "aasPath": "Traceability.ManufacturingRecordSet.ManufacturingStateRecord_00", + "aasModelType": "SubmodelElementCollection", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "Traceability.manufacturingRecordSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "manufacturingRecord_%02d" + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Pinned AAS idShort is ManufacturingStateRecord_00" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "traceability.manufacturingRecordSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "Traceability.ManufacturingRecordSet.ManufacturingStateRecord_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "Pinned AAS idShort is ManufacturingStateRecord_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "Traceability.ManufacturingRecordSet.ManufacturingStateRecord_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Traceability.manufacturingRecordSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Pinned AAS idShort is ManufacturingStateRecord_00" + }, + "explicitRequiredness": "DPP4FUN_RECORD_NAME_REQUIRED" + }, + { + "path": "traceability.manufacturingRecordSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "Traceability.ManufacturingRecordSet.ManufacturingStateRecord_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "Pinned AAS idShort is ManufacturingStateRecord_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "Traceability.ManufacturingRecordSet.ManufacturingStateRecord_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Traceability.manufacturingRecordSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Pinned AAS idShort is ManufacturingStateRecord_00" + }, + "explicitRequiredness": "DPP4FUN_RECORD_DESCRIPTION_REQUIRED" + }, + { + "path": "traceability.manufacturingRecordSet[*].operator", + "elementId": "operator", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Operator", + "Traceability.ManufacturingRecordSet.ManufacturingStateRecord_00.Operator" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "Pinned AAS idShort is ManufacturingStateRecord_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Operator", + "aasPath": "Traceability.ManufacturingRecordSet.ManufacturingStateRecord_00.Operator", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Traceability.manufacturingRecordSet[].operator", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Pinned AAS idShort is ManufacturingStateRecord_00" + }, + "explicitRequiredness": "DPP4FUN_RECORD_OPERATOR_REQUIRED" + }, + { + "path": "traceability.manufacturingRecordSet[*].startTime", + "elementId": "startTime", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:dateTime", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "StartTime", + "Traceability.ManufacturingRecordSet.ManufacturingStateRecord_00.StartTime" + ], + "source": { + "aasValueType": "xs:dateTime", + "conflictNote": "Pinned AAS idShort is ManufacturingStateRecord_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "StartTime", + "aasPath": "Traceability.ManufacturingRecordSet.ManufacturingStateRecord_00.StartTime", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Traceability.manufacturingRecordSet[].startTime", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Pinned AAS idShort is ManufacturingStateRecord_00" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "traceability.manufacturingRecordSet[*].endTime", + "elementId": "endTime", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:dateTime", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "EndTime", + "Traceability.ManufacturingRecordSet.ManufacturingStateRecord_00.EndTime" + ], + "source": { + "aasValueType": "xs:dateTime", + "conflictNote": "Pinned AAS idShort is ManufacturingStateRecord_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "EndTime", + "aasPath": "Traceability.ManufacturingRecordSet.ManufacturingStateRecord_00.EndTime", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Traceability.manufacturingRecordSet[].endTime", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Pinned AAS idShort is ManufacturingStateRecord_00" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "traceability.manufacturingRecordSet[*].station", + "elementId": "station", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Station", + "Traceability.ManufacturingRecordSet.ManufacturingStateRecord_00.Station" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "Pinned AAS idShort is ManufacturingStateRecord_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Station", + "aasPath": "Traceability.ManufacturingRecordSet.ManufacturingStateRecord_00.Station", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Traceability.manufacturingRecordSet[].station", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Pinned AAS idShort is ManufacturingStateRecord_00" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "traceability.manufacturingRecordSet[*].status", + "elementId": "status", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Status", + "Traceability.ManufacturingRecordSet.ManufacturingStateRecord_00.Status" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "Pinned AAS idShort is ManufacturingStateRecord_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Status", + "aasPath": "Traceability.ManufacturingRecordSet.ManufacturingStateRecord_00.Status", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Traceability.manufacturingRecordSet[].status", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Pinned AAS idShort is ManufacturingStateRecord_00" + }, + "explicitRequiredness": "DPP4FUN_RECORD_STATUS_REQUIRED" + }, + { + "path": "traceability.manufacturingRecordSet[*].positionInProcessRouting", + "elementId": "positionInProcessRouting", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "PositionInProcessRouting", + "Traceability.ManufacturingRecordSet.ManufacturingStateRecord_00.PositionInProcessRouting", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "conflictNote": "Pinned AAS idShort is ManufacturingStateRecord_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "PositionInProcessRouting", + "aasPath": "Traceability.ManufacturingRecordSet.ManufacturingStateRecord_00.PositionInProcessRouting", + "aasModelType": "ReferenceElement", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Traceability.manufacturingRecordSet[].positionInProcessRouting", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Pinned AAS idShort is ManufacturingStateRecord_00" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "traceability.trackingRecordSet", + "elementId": "trackingRecordSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "TrackingRecordSet", + "Traceability.TrackingRecordSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "TrackingRecordSet", + "aasPath": "Traceability.TrackingRecordSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "Traceability.trackingRecordSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "traceability.trackingRecordSet[*]", + "elementId": "trackingRecord", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#TrackingRecord", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "TrackingRecord", + "Traceability.TrackingRecordSet.TrackingRecord_00", + "http://www.w3id.org/pdto#TrackingRecord" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "TrackingRecord", + "aasPath": "Traceability.TrackingRecordSet.TrackingRecord_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "Traceability.trackingRecordSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "trackingRecord_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "traceability.trackingRecordSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "Traceability.TrackingRecordSet.TrackingRecord_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "Traceability.TrackingRecordSet.TrackingRecord_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Traceability.trackingRecordSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_RECORD_NAME_REQUIRED" + }, + { + "path": "traceability.trackingRecordSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "Traceability.TrackingRecordSet.TrackingRecord_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "Traceability.TrackingRecordSet.TrackingRecord_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Traceability.trackingRecordSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_RECORD_DESCRIPTION_REQUIRED" + }, + { + "path": "traceability.trackingRecordSet[*].operator", + "elementId": "operator", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Operator", + "Traceability.TrackingRecordSet.TrackingRecord_00.Operator" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Operator", + "aasPath": "Traceability.TrackingRecordSet.TrackingRecord_00.Operator", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Traceability.trackingRecordSet[].operator", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_RECORD_OPERATOR_REQUIRED" + }, + { + "path": "traceability.trackingRecordSet[*].time", + "elementId": "time", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:dateTime", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Time", + "Traceability.TrackingRecordSet.TrackingRecord_00.Time" + ], + "source": { + "aasValueType": "xs:dateTime", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Time", + "aasPath": "Traceability.TrackingRecordSet.TrackingRecord_00.Time", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Traceability.trackingRecordSet[].time", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_RECORD_TIME_REQUIRED" + }, + { + "path": "traceability.trackingRecordSet[*].status", + "elementId": "status", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Status", + "Traceability.TrackingRecordSet.TrackingRecord_00.Status" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Status", + "aasPath": "Traceability.TrackingRecordSet.TrackingRecord_00.Status", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Traceability.trackingRecordSet[].status", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_RECORD_STATUS_REQUIRED" + }, + { + "path": "traceability.trackingRecordSet[*].location", + "elementId": "location", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Location", + "Traceability.TrackingRecordSet.TrackingRecord_00.Location" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Location", + "aasPath": "Traceability.TrackingRecordSet.TrackingRecord_00.Location", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Traceability.trackingRecordSet[].location", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "traceability.trackingRecordSet[*].origin", + "elementId": "origin", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Origin", + "Traceability.TrackingRecordSet.TrackingRecord_00.Origin" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Origin", + "aasPath": "Traceability.TrackingRecordSet.TrackingRecord_00.Origin", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Traceability.trackingRecordSet[].origin", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "traceability.trackingRecordSet[*].destination", + "elementId": "destination", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Destination", + "Traceability.TrackingRecordSet.TrackingRecord_00.Destination" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Destination", + "aasPath": "Traceability.TrackingRecordSet.TrackingRecord_00.Destination", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Traceability.trackingRecordSet[].destination", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.16", + "figureNumber": 18, + "aasTemplate": "CIR4FUN.Traceability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "upgradability", + "elementId": "upgradability", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Upgradability", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Upgradability", + "https://www.cir4fun.eu/ontology/dpp4fun#Upgradability" + ], + "source": { + "aasModelType": "Submodel", + "d4Name": "Upgradability", + "aasPath": "Upgradability", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "EN root elementId uniqueness; D4.1 aspect presence not mandated" + }, + "javaConcept": "Upgradability", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.17", + "figureNumber": 19, + "aasTemplate": "CIR4FUN.Upgradability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "upgradability.upgradeOptionsAvailable", + "elementId": "upgradeOptionsAvailable", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "UpgradeOptionsAvailable", + "Upgradability.UpgradeOptionsAvailable" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "UpgradeOptionsAvailable", + "aasPath": "Upgradability.UpgradeOptionsAvailable", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Upgradability.upgradeOptionsAvailable", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.17", + "figureNumber": 19, + "aasTemplate": "CIR4FUN.Upgradability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "upgradability.upgradeInstructionSet", + "elementId": "upgradeInstructionSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "UpgradeInstructionSet", + "Upgradability.UpgradeInstructionSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "UpgradeInstructionSet", + "aasPath": "Upgradability.UpgradeInstructionSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "Upgradability.upgradeInstructionSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.17", + "figureNumber": 19, + "aasTemplate": "CIR4FUN.Upgradability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "upgradability.upgradeInstructionSet[*]", + "elementId": "upgradeInstruction", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#UpgradeInstruction", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "UpgradeInstruction", + "Upgradability.UpgradeInstructionSet.UpgradeInstruction_00", + "http://www.w3id.org/pdto#UpgradeInstruction" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "UpgradeInstruction", + "aasPath": "Upgradability.UpgradeInstructionSet.UpgradeInstruction_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "Upgradability.upgradeInstructionSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.17", + "figureNumber": 19, + "aasTemplate": "CIR4FUN.Upgradability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "upgradeInstruction_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "upgradability.upgradeInstructionSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "Upgradability.UpgradeInstructionSet.UpgradeInstruction_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "Upgradability.UpgradeInstructionSet.UpgradeInstruction_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Upgradability.upgradeInstructionSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.17", + "figureNumber": 19, + "aasTemplate": "CIR4FUN.Upgradability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_NAME_REQUIRED" + }, + { + "path": "upgradability.upgradeInstructionSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "Upgradability.UpgradeInstructionSet.UpgradeInstruction_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "Upgradability.UpgradeInstructionSet.UpgradeInstruction_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Upgradability.upgradeInstructionSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.17", + "figureNumber": 19, + "aasTemplate": "CIR4FUN.Upgradability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_DESCRIPTION_REQUIRED" + }, + { + "path": "upgradability.upgradeInstructionSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "Upgradability.UpgradeInstructionSet.UpgradeInstruction_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "Reference", + "aasPath": "Upgradability.UpgradeInstructionSet.UpgradeInstruction_00.Reference", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Upgradability.upgradeInstructionSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.17", + "figureNumber": 19, + "aasTemplate": "CIR4FUN.Upgradability.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_REFERENCE_REQUIRED" + }, + { + "path": "usage", + "elementId": "usage", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Usage", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Usage", + "https://www.cir4fun.eu/ontology/dpp4fun#Usage" + ], + "source": { + "aasModelType": "Submodel", + "d4Name": "Usage", + "aasPath": "Usage", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "EN root elementId uniqueness; D4.1 aspect presence not mandated" + }, + "javaConcept": "Usage", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.18", + "figureNumber": 20, + "aasTemplate": "CIR4FUN.Usage.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "usage.operatingHours", + "elementId": "operatingHours", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:integer", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "OperatingHours", + "Usage.OperatingHours" + ], + "source": { + "aasValueType": "xs:integer", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "OperatingHours", + "aasPath": "Usage.OperatingHours", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Usage.operatingHours", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.18", + "figureNumber": 20, + "aasTemplate": "CIR4FUN.Usage.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "usage.primaryFunction", + "elementId": "primaryFunction", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#PrimaryFunction", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "PrimaryFunction", + "Usage.PrimaryFunction", + "https://www.cir4fun.eu/ontology/dpp4fun#PrimaryFunction" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "PrimaryFunction", + "aasPath": "Usage.PrimaryFunction", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Usage.primaryFunction", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.18", + "figureNumber": 20, + "aasTemplate": "CIR4FUN.Usage.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "usage.primaryFunction.name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "Usage.PrimaryFunction.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "Usage.PrimaryFunction.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Usage.primaryFunction.name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.18", + "figureNumber": 20, + "aasTemplate": "CIR4FUN.Usage.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "usage.primaryFunction.description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "Usage.PrimaryFunction.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "Usage.PrimaryFunction.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Usage.primaryFunction.description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.18", + "figureNumber": 20, + "aasTemplate": "CIR4FUN.Usage.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "usage.primaryFunction.status", + "elementId": "status", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Status", + "Usage.PrimaryFunction.Status" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Status", + "aasPath": "Usage.PrimaryFunction.Status", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Usage.primaryFunction.status", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.18", + "figureNumber": 20, + "aasTemplate": "CIR4FUN.Usage.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "usage.secondaryFunctionSet", + "elementId": "secondaryFunctionSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "SecondaryFunctionSet", + "Usage.SecondaryFunctionSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "SecondaryFunctionSet", + "aasPath": "Usage.SecondaryFunctionSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "Usage.secondaryFunctionSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.18", + "figureNumber": 20, + "aasTemplate": "CIR4FUN.Usage.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "usage.secondaryFunctionSet[*]", + "elementId": "secondaryFunction", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#SecondaryFunction", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "SecondaryFunction", + "Usage.SecondaryFunctionSet.SecondaryFunction_00", + "https://www.cir4fun.eu/ontology/dpp4fun#SecondaryFunction" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "SecondaryFunction", + "aasPath": "Usage.SecondaryFunctionSet.SecondaryFunction_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "Usage.secondaryFunctionSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.18", + "figureNumber": 20, + "aasTemplate": "CIR4FUN.Usage.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "secondaryFunction_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "usage.secondaryFunctionSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "Usage.SecondaryFunctionSet.SecondaryFunction_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "Usage.SecondaryFunctionSet.SecondaryFunction_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Usage.secondaryFunctionSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.18", + "figureNumber": 20, + "aasTemplate": "CIR4FUN.Usage.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "usage.secondaryFunctionSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "Usage.SecondaryFunctionSet.SecondaryFunction_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "Usage.SecondaryFunctionSet.SecondaryFunction_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Usage.secondaryFunctionSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.18", + "figureNumber": 20, + "aasTemplate": "CIR4FUN.Usage.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "usage.secondaryFunctionSet[*].status", + "elementId": "status", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Status", + "Usage.SecondaryFunctionSet.SecondaryFunction_00.Status" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Status", + "aasPath": "Usage.SecondaryFunctionSet.SecondaryFunction_00.Status", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Usage.secondaryFunctionSet[].status", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.18", + "figureNumber": 20, + "aasTemplate": "CIR4FUN.Usage.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "usage.assemblyInstructionSet", + "elementId": "assemblyInstructionSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "AssemblyInstructionSet", + "Usage.AssemblyInstructionSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "AssemblyInstructionSet", + "aasPath": "Usage.AssemblyInstructionSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "Usage.assemblyInstructionSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.18", + "figureNumber": 20, + "aasTemplate": "CIR4FUN.Usage.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "usage.assemblyInstructionSet[*]", + "elementId": "assemblyInstruction", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#AssemblyInstruction", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "AssemblyInstruction", + "Usage.AssemblyInstructionSet.AssemblyInstruction_00", + "http://www.w3id.org/pdto#AssemblyInstruction" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "AssemblyInstruction", + "aasPath": "Usage.AssemblyInstructionSet.AssemblyInstruction_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "Usage.assemblyInstructionSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.18", + "figureNumber": 20, + "aasTemplate": "CIR4FUN.Usage.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "assemblyInstruction_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "usage.assemblyInstructionSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "Usage.AssemblyInstructionSet.AssemblyInstruction_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "Usage.AssemblyInstructionSet.AssemblyInstruction_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Usage.assemblyInstructionSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.18", + "figureNumber": 20, + "aasTemplate": "CIR4FUN.Usage.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_NAME_REQUIRED" + }, + { + "path": "usage.assemblyInstructionSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "Usage.AssemblyInstructionSet.AssemblyInstruction_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "Usage.AssemblyInstructionSet.AssemblyInstruction_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Usage.assemblyInstructionSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.18", + "figureNumber": 20, + "aasTemplate": "CIR4FUN.Usage.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_DESCRIPTION_REQUIRED" + }, + { + "path": "usage.assemblyInstructionSet[*].targetAudience", + "elementId": "targetAudience", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "TargetAudience", + "Usage.AssemblyInstructionSet.AssemblyInstruction_00.TargetAudience" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "TargetAudience", + "aasPath": "Usage.AssemblyInstructionSet.AssemblyInstruction_00.TargetAudience", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Usage.assemblyInstructionSet[].targetAudience", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.18", + "figureNumber": 20, + "aasTemplate": "CIR4FUN.Usage.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "usage.assemblyInstructionSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "Usage.AssemblyInstructionSet.AssemblyInstruction_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "Reference", + "aasPath": "Usage.AssemblyInstructionSet.AssemblyInstruction_00.Reference", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Usage.assemblyInstructionSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.18", + "figureNumber": 20, + "aasTemplate": "CIR4FUN.Usage.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_REFERENCE_REQUIRED" + }, + { + "path": "usage.disassemblyInstructionSet", + "elementId": "disassemblyInstructionSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "DisassemblyInstructionSet", + "Usage.DisassemblyInstructionSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "DisassemblyInstructionSet", + "aasPath": "Usage.DisassemblyInstructionSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "Usage.disassemblyInstructionSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.18", + "figureNumber": 20, + "aasTemplate": "CIR4FUN.Usage.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "usage.disassemblyInstructionSet[*]", + "elementId": "disassemblyInstruction", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#DisassemblyInstruction", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "DisassemblyInstruction", + "Usage.DisassemblyInstructionSet.DiasssemblyInstruction_00", + "http://www.w3id.org/pdto#DisassemblyInstruction" + ], + "source": { + "conflictNote": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "DisassemblyInstruction", + "aasPath": "Usage.DisassemblyInstructionSet.DiasssemblyInstruction_00", + "aasModelType": "SubmodelElementCollection", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "Usage.disassemblyInstructionSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.18", + "figureNumber": 20, + "aasTemplate": "CIR4FUN.Usage.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "disassemblyInstruction_%02d" + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "usage.disassemblyInstructionSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "Usage.DisassemblyInstructionSet.DiasssemblyInstruction_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "Usage.DisassemblyInstructionSet.DiasssemblyInstruction_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Usage.disassemblyInstructionSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.18", + "figureNumber": 20, + "aasTemplate": "CIR4FUN.Usage.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction" + }, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_NAME_REQUIRED" + }, + { + "path": "usage.disassemblyInstructionSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "Usage.DisassemblyInstructionSet.DiasssemblyInstruction_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "Usage.DisassemblyInstructionSet.DiasssemblyInstruction_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Usage.disassemblyInstructionSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.18", + "figureNumber": 20, + "aasTemplate": "CIR4FUN.Usage.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction" + }, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_DESCRIPTION_REQUIRED" + }, + { + "path": "usage.disassemblyInstructionSet[*].targetAudience", + "elementId": "targetAudience", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "TargetAudience", + "Usage.DisassemblyInstructionSet.DiasssemblyInstruction_00.TargetAudience" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "TargetAudience", + "aasPath": "Usage.DisassemblyInstructionSet.DiasssemblyInstruction_00.TargetAudience", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Usage.disassemblyInstructionSet[].targetAudience", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.18", + "figureNumber": 20, + "aasTemplate": "CIR4FUN.Usage.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "usage.disassemblyInstructionSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "Usage.DisassemblyInstructionSet.DiasssemblyInstruction_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "conflictNote": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Reference", + "aasPath": "Usage.DisassemblyInstructionSet.DiasssemblyInstruction_00.Reference", + "aasModelType": "ReferenceElement", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "Usage.disassemblyInstructionSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.18", + "figureNumber": 20, + "aasTemplate": "CIR4FUN.Usage.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction" + }, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_REFERENCE_REQUIRED" + }, + { + "path": "wasteManagement", + "elementId": "wasteManagement", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#WasteManagement", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "WasteManagement", + "https://www.cir4fun.eu/ontology/dpp4fun#WasteManagement" + ], + "source": { + "aasModelType": "Submodel", + "d4Name": "WasteManagement", + "aasPath": "WasteManagement", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "EN root elementId uniqueness; D4.1 aspect presence not mandated" + }, + "javaConcept": "WasteManagement", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.19", + "figureNumber": 21, + "aasTemplate": "CIR4FUN.WasteManagement.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "wasteManagement.toxicityRisk", + "elementId": "toxicityRisk", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ToxicityRisk", + "WasteManagement.ToxicityRisk" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "D4.1 prose+UML Boolean; AAS xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ToxicityRisk", + "aasPath": "WasteManagement.ToxicityRisk", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "WasteManagement.toxicityRisk", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.19", + "figureNumber": 21, + "aasTemplate": "CIR4FUN.WasteManagement.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "D4.1 prose+UML Boolean; AAS xs:string" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "wasteManagement.fireRisk", + "elementId": "fireRisk", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "FireRisk", + "WasteManagement.FireRisk" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "D4.1 prose+UML Boolean; AAS xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "FireRisk", + "aasPath": "WasteManagement.FireRisk", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "WasteManagement.fireRisk", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.19", + "figureNumber": 21, + "aasTemplate": "CIR4FUN.WasteManagement.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "D4.1 prose+UML Boolean; AAS xs:string" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "wasteManagement.electricityRisk", + "elementId": "electricityRisk", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "ElectricityRisk", + "WasteManagement.ElectricityRisk" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "D4.1 prose+UML Boolean; AAS xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ElectricityRisk", + "aasPath": "WasteManagement.ElectricityRisk", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "WasteManagement.electricityRisk", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.19", + "figureNumber": 21, + "aasTemplate": "CIR4FUN.WasteManagement.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "D4.1 prose+UML Boolean; AAS xs:string" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "wasteManagement.disposalInstructionSet", + "elementId": "disposalInstructionSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "DisposalInstructionSet", + "WasteManagement.DisposalInstructionSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "DisposalInstructionSet", + "aasPath": "WasteManagement.DisposalInstructionSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "WasteManagement.disposalInstructionSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.19", + "figureNumber": 21, + "aasTemplate": "CIR4FUN.WasteManagement.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "wasteManagement.disposalInstructionSet[*]", + "elementId": "disposalInstruction", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#DisposalInstruction", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "DisposalInstruction", + "WasteManagement.DisposalInstructionSet.DisposalInstruction_00", + "http://www.w3id.org/pdto#DisposalInstruction" + ], + "source": { + "aasModelType": "SubmodelElementCollection", + "d4Name": "DisposalInstruction", + "aasPath": "WasteManagement.DisposalInstructionSet.DisposalInstruction_00", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "WasteManagement.disposalInstructionSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.19", + "figureNumber": 21, + "aasTemplate": "CIR4FUN.WasteManagement.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "disposalInstruction_%02d" + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "wasteManagement.disposalInstructionSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "WasteManagement.DisposalInstructionSet.DisposalInstruction_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "WasteManagement.DisposalInstructionSet.DisposalInstruction_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "WasteManagement.disposalInstructionSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.19", + "figureNumber": 21, + "aasTemplate": "CIR4FUN.WasteManagement.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_NAME_REQUIRED" + }, + { + "path": "wasteManagement.disposalInstructionSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "WasteManagement.DisposalInstructionSet.DisposalInstruction_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "WasteManagement.DisposalInstructionSet.DisposalInstruction_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "WasteManagement.disposalInstructionSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.19", + "figureNumber": 21, + "aasTemplate": "CIR4FUN.WasteManagement.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_DESCRIPTION_REQUIRED" + }, + { + "path": "wasteManagement.disposalInstructionSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "WasteManagement.DisposalInstructionSet.DisposalInstruction_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "aasModelType": "ReferenceElement", + "d4Name": "Reference", + "aasPath": "WasteManagement.DisposalInstructionSet.DisposalInstruction_00.Reference", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "WasteManagement.disposalInstructionSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.19", + "figureNumber": 21, + "aasTemplate": "CIR4FUN.WasteManagement.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_REFERENCE_REQUIRED" + }, + { + "path": "wasteManagement.disassemblyInstructionSet", + "elementId": "disassemblyInstructionSet", + "objectType": "MultiValuedDataElement", + "dictionaryReference": null, + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "DisassemblyInstructionSet", + "WasteManagement.DisassemblyInstructionSet" + ], + "source": { + "aasModelType": "SubmodelElementList", + "d4Name": "DisassemblyInstructionSet", + "aasPath": "WasteManagement.DisassemblyInstructionSet", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "cardinalityProvenance": "container multiplicity not stated" + }, + "javaConcept": "WasteManagement.disassemblyInstructionSet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.19", + "figureNumber": 21, + "aasTemplate": "CIR4FUN.WasteManagement.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_LIST_ITEM_KIND", + "DPP4FUN_PROFILE_LIST_ORDER_NOT_PRESERVED" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "wasteManagement.disassemblyInstructionSet[*]", + "elementId": "disassemblyInstruction", + "objectType": "DataElementCollection", + "dictionaryReference": "http://www.w3id.org/pdto#DisassemblyInstruction", + "valueDataType": null, + "sourceCardinality": { + "min": 0, + "max": null + }, + "bindingOccurrence": { + "min": 0, + "max": null + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "DisassemblyInstruction", + "WasteManagement.DisassemblyInstructionSet.DiasssemblyInstruction_00", + "http://www.w3id.org/pdto#DisassemblyInstruction" + ], + "source": { + "conflictNote": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "DisassemblyInstruction", + "aasPath": "WasteManagement.DisassemblyInstructionSet.DiasssemblyInstruction_00", + "aasModelType": "SubmodelElementCollection", + "cardinalityProvenance": "D4.1 UML" + }, + "javaConcept": "WasteManagement.disassemblyInstructionSet[]", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.19", + "figureNumber": 21, + "aasTemplate": "CIR4FUN.WasteManagement.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "SOURCE_CARDINALITY_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": "disassemblyInstruction_%02d" + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "wasteManagement.disassemblyInstructionSet[*].name", + "elementId": "name", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/name", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Name", + "WasteManagement.DisassemblyInstructionSet.DiasssemblyInstruction_00.Name", + "https://schema.org/name" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Name", + "aasPath": "WasteManagement.DisassemblyInstructionSet.DiasssemblyInstruction_00.Name", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "WasteManagement.disassemblyInstructionSet[].name", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.19", + "figureNumber": 21, + "aasTemplate": "CIR4FUN.WasteManagement.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction" + }, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_NAME_REQUIRED" + }, + { + "path": "wasteManagement.disassemblyInstructionSet[*].description", + "elementId": "description", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://schema.org/description", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Description", + "WasteManagement.DisassemblyInstructionSet.DiasssemblyInstruction_00.Description", + "https://schema.org/description" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Description", + "aasPath": "WasteManagement.DisassemblyInstructionSet.DiasssemblyInstruction_00.Description", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "WasteManagement.disassemblyInstructionSet[].description", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.19", + "figureNumber": 21, + "aasTemplate": "CIR4FUN.WasteManagement.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction" + }, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_DESCRIPTION_REQUIRED" + }, + { + "path": "wasteManagement.disassemblyInstructionSet[*].targetAudience", + "elementId": "targetAudience", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "TargetAudience", + "WasteManagement.DisassemblyInstructionSet.DiasssemblyInstruction_00.TargetAudience" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "TargetAudience", + "aasPath": "WasteManagement.DisassemblyInstructionSet.DiasssemblyInstruction_00.TargetAudience", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "WasteManagement.disassemblyInstructionSet[].targetAudience", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.19", + "figureNumber": 21, + "aasTemplate": "CIR4FUN.WasteManagement.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "wasteManagement.disassemblyInstructionSet[*].reference", + "elementId": "reference", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "http://www.w3id.org/pdto#hasSimpleReference", + "valueDataType": "xsd:anyURI", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "general" + ], + "aliases": [ + "Reference", + "WasteManagement.DisassemblyInstructionSet.DiasssemblyInstruction_00.Reference", + "http://www.w3id.org/pdto#hasSimpleReference" + ], + "source": { + "conflictNote": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Reference", + "aasPath": "WasteManagement.DisassemblyInstructionSet.DiasssemblyInstruction_00.Reference", + "aasModelType": "ReferenceElement", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "WasteManagement.disassemblyInstructionSet[].reference", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.1.19", + "figureNumber": 21, + "aasTemplate": "CIR4FUN.WasteManagement.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH", + "DPP4FUN_URI_FIELD_RFC3986", + "REPEATED_ITEM_ID_MISMATCH" + ], + "repeatedItemPolicy": { + "orderPreserved": true, + "elementIdPattern": null + }, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Pinned AAS idShort uses source typo DiasssemblyInstruction_00; canonical EN elementId remains disassemblyInstruction" + }, + "explicitRequiredness": "DPP4FUN_INSTRUCTION_REFERENCE_REQUIRED" + }, + { + "path": "characteristics", + "elementId": "characteristics", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Characteristics", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "Characteristics", + "https://www.cir4fun.eu/ontology/dpp4fun#Characteristics" + ], + "source": { + "conflictNote": "product-specific template", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Characteristics", + "aasPath": "Characteristics", + "aasModelType": "Submodel", + "cardinalityProvenance": "EN root elementId uniqueness; D4.1 aspect presence not mandated" + }, + "javaConcept": "ChairCharacteristics", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "product-specific template" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.length", + "elementId": "length", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Length", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "lengthUnit", + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "Length", + "Characteristics.Length", + "https://www.cir4fun.eu/ontology/dpp4fun#Length" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Length", + "aasPath": "Characteristics.Length", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.length", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.width", + "elementId": "width", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Width", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "widthUnit", + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "Width", + "Characteristics.Width", + "https://www.cir4fun.eu/ontology/dpp4fun#Width" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Width", + "aasPath": "Characteristics.Width", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.width", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.height", + "elementId": "height", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Height", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "heightUnit", + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "Height", + "Characteristics.Height", + "https://www.cir4fun.eu/ontology/dpp4fun#Height" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Height", + "aasPath": "Characteristics.Height", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.height", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.diameter", + "elementId": "diameter", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Diameter", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "diameterUnit", + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "Diameter", + "Characteristics.Diameter", + "https://www.cir4fun.eu/ontology/dpp4fun#Diameter" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Diameter", + "aasPath": "Characteristics.Diameter", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.diameter", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.weight", + "elementId": "weight", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Weight", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "mass", + "unitSlotPath": "weightUnit", + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "Weight", + "Characteristics.Weight", + "https://www.cir4fun.eu/ontology/dpp4fun#Weight" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Weight", + "aasPath": "Characteristics.Weight", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.weight", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.volume", + "elementId": "volume", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Volumn", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "volume", + "unitSlotPath": "volumeUnit", + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "Volume", + "Characteristics.Volume", + "https://www.cir4fun.eu/ontology/dpp4fun#Volumn" + ], + "source": { + "aasValueType": "xs:double", + "conflictNote": "Published ontology IRI is misspelled #Volumn; keep external IRI, canonical EN elementId is volume", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Volume", + "aasPath": "Characteristics.Volume", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.volume", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Published ontology IRI is misspelled #Volumn; keep external IRI, canonical EN elementId is volume" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.surface", + "elementId": "surface", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "Surface", + "Characteristics.Surface", + "https://www.cir4fun.eu/ontology/dpp4fun#Surface" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "D4.1 prose describes numeric total area as Double, while its UML, six pinned AAS templates, and populated examples define Surface as a String surface type. Follow the published AAS binding for furniture Characteristics.", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Surface", + "aasPath": "Characteristics.Surface", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED", + "resolutionStatus": "RESOLVED_WITHOUT_DICTIONARY_REFERENCE", + "resolutionNote": "Follow the published AAS template and populated examples for Characteristics Surface: xsd:string describing surface type. D4.1 numeric total-area prose conflicts with its UML and published data; do not invent a SurfaceArea field or semantic IRI." + }, + "javaConcept": "ChairCharacteristics.surface", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITHOUT_DICTIONARY_REFERENCE", + "conflictResolution": { + "note": "Use xsd:string per the six published AAS templates and populated examples; D4.1 numeric total-area prose conflicts with UML and published data. Keep dictionaryReference null; no semantic IRI is added." + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.color", + "elementId": "color", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Color", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "Color", + "Characteristics.Color", + "https://www.cir4fun.eu/ontology/dpp4fun#Color" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "D4.1 prose uses Colour spelling; AAS+ontology publish Color", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Color", + "aasPath": "Characteristics.Color", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.color", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "D4.1 prose uses Colour spelling; AAS+ontology publish Color" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.lengthUnit", + "elementId": "lengthUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "LengthUnit", + "Characteristics.LengthUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "LengthUnit", + "aasPath": "Characteristics.LengthUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.lengthUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.widthUnit", + "elementId": "widthUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "WidthUnit", + "Characteristics.WidthUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "WidthUnit", + "aasPath": "Characteristics.WidthUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.widthUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.heightUnit", + "elementId": "heightUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "HeightUnit", + "Characteristics.HeightUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "HeightUnit", + "aasPath": "Characteristics.HeightUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.heightUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.diameterUnit", + "elementId": "diameterUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "DiameterUnit", + "Characteristics.DiameterUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "DiameterUnit", + "aasPath": "Characteristics.DiameterUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.diameterUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.weightUnit", + "elementId": "weightUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "mass-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "WeightUnit", + "Characteristics.WeightUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "WeightUnit", + "aasPath": "Characteristics.WeightUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.weightUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.volumeUnit", + "elementId": "volumeUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "volume-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "VolumeUnit", + "Characteristics.VolumeUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "VolumeUnit", + "aasPath": "Characteristics.VolumeUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.volumeUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.seatHeight", + "elementId": "seatHeight", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#SeatHeight", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "heightUnit", + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "SeatHeight", + "Characteristics.SeatHeight", + "https://www.cir4fun.eu/ontology/dpp4fun#SeatHeight" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "SeatHeight", + "aasPath": "Characteristics.SeatHeight", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.seatHeight", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.seatWidth", + "elementId": "seatWidth", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#SeatWidth", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "widthUnit", + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "SeatWidth", + "Characteristics.SeatWidth", + "https://www.cir4fun.eu/ontology/dpp4fun#SeatWidth" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "SeatWidth", + "aasPath": "Characteristics.SeatWidth", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.seatWidth", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.seatLength", + "elementId": "seatLength", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#SeatLength", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "lengthUnit", + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "SeatLength", + "Characteristics.SeatLength", + "https://www.cir4fun.eu/ontology/dpp4fun#SeatLength" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "SeatLength", + "aasPath": "Characteristics.SeatLength", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.seatLength", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.armrestHeight", + "elementId": "armrestHeight", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#ArmrestHeight", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "heightUnit", + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "ArmrestHeight", + "Characteristics.ArmrestHeight", + "https://www.cir4fun.eu/ontology/dpp4fun#ArmrestHeight" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ArmrestHeight", + "aasPath": "Characteristics.ArmrestHeight", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.armrestHeight", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.armrestWidth", + "elementId": "armrestWidth", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#ArmrestWidth", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "widthUnit", + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "ArmrestWidth", + "Characteristics.ArmrestWidth", + "https://www.cir4fun.eu/ontology/dpp4fun#ArmrestWidth" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ArmrestWidth", + "aasPath": "Characteristics.ArmrestWidth", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.armrestWidth", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.backrestHeight", + "elementId": "backrestHeight", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#BackrestHeight", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "heightUnit", + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "BackrestHeight", + "Characteristics.BackrestHeight", + "https://www.cir4fun.eu/ontology/dpp4fun#BackrestHeight" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "BackrestHeight", + "aasPath": "Characteristics.BackrestHeight", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.backrestHeight", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.legHeight", + "elementId": "legHeight", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#LegHeight", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "heightUnit", + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "LegHeight", + "Characteristics.LegHeight", + "https://www.cir4fun.eu/ontology/dpp4fun#LegHeight" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "LegHeight", + "aasPath": "Characteristics.LegHeight", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.legHeight", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.foamHardnessLevel", + "elementId": "foamHardnessLevel", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#FoamHardnessLevel", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "FoamHardnessLevel", + "Characteristics.FoamHardnessLevel", + "https://www.cir4fun.eu/ontology/dpp4fun#FoamHardnessLevel" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "FoamHardnessLevel", + "aasPath": "Characteristics.FoamHardnessLevel", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.foamHardnessLevel", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.seatingCapacity", + "elementId": "seatingCapacity", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#SeatingCapacity", + "valueDataType": "xsd:integer", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "SeatingCapacity", + "Characteristics.SeatingCapacity", + "https://www.cir4fun.eu/ontology/dpp4fun#SeatingCapacity" + ], + "source": { + "aasValueType": "xs:integer", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "SeatingCapacity", + "aasPath": "Characteristics.SeatingCapacity", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.seatingCapacity", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.indoorUse", + "elementId": "indoorUse", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#IndoorUse", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "IndoorUse", + "Characteristics.IndoorUse", + "https://www.cir4fun.eu/ontology/dpp4fun#IndoorUse" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "IndoorUse", + "aasPath": "Characteristics.IndoorUse", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.indoorUse", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.outdoorUse", + "elementId": "outdoorUse", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#OutdoorUse", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "OutdoorUse", + "Characteristics.OutdoorUse", + "https://www.cir4fun.eu/ontology/dpp4fun#OutdoorUse" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "OutdoorUse", + "aasPath": "Characteristics.OutdoorUse", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.outdoorUse", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.reclining", + "elementId": "reclining", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#RecliningSystem", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "Reclining", + "Characteristics.Reclining", + "https://www.cir4fun.eu/ontology/dpp4fun#RecliningSystem" + ], + "source": { + "aasValueType": "xs:boolean", + "conflictNote": "AAS name Reclining; ontology term is #RecliningSystem", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Reclining", + "aasPath": "Characteristics.Reclining", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.reclining", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "AAS name Reclining; ontology term is #RecliningSystem" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.seatSliding", + "elementId": "seatSliding", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#SeatSliding", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "SeatSliding", + "Characteristics.SeatSliding", + "https://www.cir4fun.eu/ontology/dpp4fun#SeatSliding" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "SeatSliding", + "aasPath": "Characteristics.SeatSliding", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.seatSliding", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.swivel", + "elementId": "swivel", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Swivel", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "Swivel", + "Characteristics.Swivel", + "https://www.cir4fun.eu/ontology/dpp4fun#Swivel" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Swivel", + "aasPath": "Characteristics.Swivel", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.swivel", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.waterRepellent", + "elementId": "waterRepellent", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#WaterRepellent", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "WaterRepellent", + "Characteristics.WaterRepellent", + "https://www.cir4fun.eu/ontology/dpp4fun#WaterRepellent" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "WaterRepellent", + "aasPath": "Characteristics.WaterRepellent", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.waterRepellent", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.stainResistant", + "elementId": "stainResistant", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#StainResistant", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "StainResistant", + "Characteristics.StainResistant", + "https://www.cir4fun.eu/ontology/dpp4fun#StainResistant" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "StainResistant", + "aasPath": "Characteristics.StainResistant", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.stainResistant", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.easyToClean", + "elementId": "easyToClean", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#EasyToClean", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "EasyToClean", + "Characteristics.EasyToClean", + "https://www.cir4fun.eu/ontology/dpp4fun#EasyToClean" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "EasyToClean", + "aasPath": "Characteristics.EasyToClean", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.easyToClean", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.fireResistant", + "elementId": "fireResistant", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#FireResistant", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "FireResistant", + "Characteristics.FireResistant", + "https://www.cir4fun.eu/ontology/dpp4fun#FireResistant" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "FireResistant", + "aasPath": "Characteristics.FireResistant", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.fireResistant", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.seatRemovableCover", + "elementId": "seatRemovableCover", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#SeatCoverRemovable", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "SeatRemovableCover", + "Characteristics.SeatRemovableCover", + "https://www.cir4fun.eu/ontology/dpp4fun#SeatCoverRemovable" + ], + "source": { + "aasValueType": "xs:boolean", + "conflictNote": "AAS/D4.1 name vs ontology alias", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "SeatRemovableCover", + "aasPath": "Characteristics.SeatRemovableCover", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.seatRemovableCover", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "AAS/D4.1 name vs ontology alias" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.backrestRemovableCover", + "elementId": "backrestRemovableCover", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#BackrestCoverRemovable", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "BackrestRemovableCover", + "Characteristics.BackrestRemovableCover", + "https://www.cir4fun.eu/ontology/dpp4fun#BackrestCoverRemovable" + ], + "source": { + "aasValueType": "xs:boolean", + "conflictNote": "AAS/D4.1 name vs ontology alias", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "BackrestRemovableCover", + "aasPath": "Characteristics.BackrestRemovableCover", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.backrestRemovableCover", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "AAS/D4.1 name vs ontology alias" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.adjustableFeet", + "elementId": "adjustableFeet", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#FeetAdjustable", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "AdjustableFeet", + "Characteristics.AdjustableFeet", + "https://www.cir4fun.eu/ontology/dpp4fun#FeetAdjustable" + ], + "source": { + "aasValueType": "xs:boolean", + "conflictNote": "AAS/D4.1 name vs ontology alias", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "AdjustableFeet", + "aasPath": "Characteristics.AdjustableFeet", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.adjustableFeet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "AAS/D4.1 name vs ontology alias" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.armrestIncluded", + "elementId": "armrestIncluded", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#ArmrestIncluded", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "ArmrestIncluded", + "Characteristics.ArmrestIncluded", + "https://www.cir4fun.eu/ontology/dpp4fun#ArmrestIncluded" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ArmrestIncluded", + "aasPath": "Characteristics.ArmrestIncluded", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.armrestIncluded", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.armrestRemovableCover", + "elementId": "armrestRemovableCover", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#ArmrestCoverRemovable", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "ArmrestRemovableCover", + "Characteristics.ArmrestRemovableCover", + "https://www.cir4fun.eu/ontology/dpp4fun#ArmrestCoverRemovable" + ], + "source": { + "aasValueType": "xs:boolean", + "conflictNote": "AAS/D4.1 name vs ontology alias", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ArmrestRemovableCover", + "aasPath": "Characteristics.ArmrestRemovableCover", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.armrestRemovableCover", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "AAS/D4.1 name vs ontology alias" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.stackable", + "elementId": "stackable", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Stackable", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "Stackable", + "Characteristics.Stackable", + "https://www.cir4fun.eu/ontology/dpp4fun#Stackable" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Stackable", + "aasPath": "Characteristics.Stackable", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.stackable", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.heightAdjustable", + "elementId": "heightAdjustable", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#HeightAdjustable", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "HeightAdjustable", + "Characteristics.HeightAdjustable", + "https://www.cir4fun.eu/ontology/dpp4fun#HeightAdjustable" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "HeightAdjustable", + "aasPath": "Characteristics.HeightAdjustable", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.heightAdjustable", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.padded", + "elementId": "padded", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Padded", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "Padded", + "Characteristics.Padded", + "https://www.cir4fun.eu/ontology/dpp4fun#Padded" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Padded", + "aasPath": "Characteristics.Padded", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.padded", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.payload", + "elementId": "payload", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Payload", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "mass", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-chair" + ], + "aliases": [ + "Payload", + "Characteristics.Payload", + "https://www.cir4fun.eu/ontology/dpp4fun#Payload" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Payload", + "aasPath": "Characteristics.Payload", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ChairCharacteristics.payload", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.1", + "figureNumber": 22, + "aasTemplate": "CIR4FUN.ChairCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics", + "elementId": "characteristics", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Characteristics", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-crib" + ], + "aliases": [ + "Characteristics", + "https://www.cir4fun.eu/ontology/dpp4fun#Characteristics" + ], + "source": { + "conflictNote": "product-specific template", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Characteristics", + "aasPath": "Characteristics", + "aasModelType": "Submodel", + "cardinalityProvenance": "EN root elementId uniqueness; D4.1 aspect presence not mandated" + }, + "javaConcept": "CribCharacteristics", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.2", + "figureNumber": 23, + "aasTemplate": "CIR4FUN.CribCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "product-specific template" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.length", + "elementId": "length", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Length", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "lengthUnit", + "appliesTo": [ + "characteristics-crib" + ], + "aliases": [ + "Length", + "Characteristics.Length", + "https://www.cir4fun.eu/ontology/dpp4fun#Length" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Length", + "aasPath": "Characteristics.Length", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "CribCharacteristics.length", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.2", + "figureNumber": 23, + "aasTemplate": "CIR4FUN.CribCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.width", + "elementId": "width", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Width", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "widthUnit", + "appliesTo": [ + "characteristics-crib" + ], + "aliases": [ + "Width", + "Characteristics.Width", + "https://www.cir4fun.eu/ontology/dpp4fun#Width" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Width", + "aasPath": "Characteristics.Width", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "CribCharacteristics.width", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.2", + "figureNumber": 23, + "aasTemplate": "CIR4FUN.CribCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.height", + "elementId": "height", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Height", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "heightUnit", + "appliesTo": [ + "characteristics-crib" + ], + "aliases": [ + "Height", + "Characteristics.Height", + "https://www.cir4fun.eu/ontology/dpp4fun#Height" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Height", + "aasPath": "Characteristics.Height", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "CribCharacteristics.height", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.2", + "figureNumber": 23, + "aasTemplate": "CIR4FUN.CribCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.diameter", + "elementId": "diameter", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Diameter", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "diameterUnit", + "appliesTo": [ + "characteristics-crib" + ], + "aliases": [ + "Diameter", + "Characteristics.Diameter", + "https://www.cir4fun.eu/ontology/dpp4fun#Diameter" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Diameter", + "aasPath": "Characteristics.Diameter", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "CribCharacteristics.diameter", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.2", + "figureNumber": 23, + "aasTemplate": "CIR4FUN.CribCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.weight", + "elementId": "weight", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Weight", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "mass", + "unitSlotPath": "weightUnit", + "appliesTo": [ + "characteristics-crib" + ], + "aliases": [ + "Weight", + "Characteristics.Weight", + "https://www.cir4fun.eu/ontology/dpp4fun#Weight" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Weight", + "aasPath": "Characteristics.Weight", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "CribCharacteristics.weight", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.2", + "figureNumber": 23, + "aasTemplate": "CIR4FUN.CribCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.volume", + "elementId": "volume", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Volumn", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "volume", + "unitSlotPath": "volumeUnit", + "appliesTo": [ + "characteristics-crib" + ], + "aliases": [ + "Volume", + "Characteristics.Volume", + "https://www.cir4fun.eu/ontology/dpp4fun#Volumn" + ], + "source": { + "aasValueType": "xs:double", + "conflictNote": "Published ontology IRI is misspelled #Volumn; keep external IRI, canonical EN elementId is volume", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Volume", + "aasPath": "Characteristics.Volume", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "CribCharacteristics.volume", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.2", + "figureNumber": 23, + "aasTemplate": "CIR4FUN.CribCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Published ontology IRI is misspelled #Volumn; keep external IRI, canonical EN elementId is volume" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.surface", + "elementId": "surface", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-crib" + ], + "aliases": [ + "Surface", + "Characteristics.Surface", + "https://www.cir4fun.eu/ontology/dpp4fun#Surface" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "D4.1 prose describes numeric total area as Double, while its UML, six pinned AAS templates, and populated examples define Surface as a String surface type. Follow the published AAS binding for furniture Characteristics.", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Surface", + "aasPath": "Characteristics.Surface", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED", + "resolutionStatus": "RESOLVED_WITHOUT_DICTIONARY_REFERENCE", + "resolutionNote": "Follow the published AAS template and populated examples for Characteristics Surface: xsd:string describing surface type. D4.1 numeric total-area prose conflicts with its UML and published data; do not invent a SurfaceArea field or semantic IRI." + }, + "javaConcept": "CribCharacteristics.surface", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.2", + "figureNumber": 23, + "aasTemplate": "CIR4FUN.CribCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITHOUT_DICTIONARY_REFERENCE", + "conflictResolution": { + "note": "Use xsd:string per the six published AAS templates and populated examples; D4.1 numeric total-area prose conflicts with UML and published data. Keep dictionaryReference null; no semantic IRI is added." + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.color", + "elementId": "color", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Color", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-crib" + ], + "aliases": [ + "Color", + "Characteristics.Color", + "https://www.cir4fun.eu/ontology/dpp4fun#Color" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "D4.1 prose uses Colour spelling; AAS+ontology publish Color", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Color", + "aasPath": "Characteristics.Color", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "CribCharacteristics.color", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.2", + "figureNumber": 23, + "aasTemplate": "CIR4FUN.CribCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "D4.1 prose uses Colour spelling; AAS+ontology publish Color" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.lengthUnit", + "elementId": "lengthUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-crib" + ], + "aliases": [ + "LengthUnit", + "Characteristics.LengthUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "LengthUnit", + "aasPath": "Characteristics.LengthUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "CribCharacteristics.lengthUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.2", + "figureNumber": 23, + "aasTemplate": "CIR4FUN.CribCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.widthUnit", + "elementId": "widthUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-crib" + ], + "aliases": [ + "WidthUnit", + "Characteristics.WidthUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "WidthUnit", + "aasPath": "Characteristics.WidthUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "CribCharacteristics.widthUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.2", + "figureNumber": 23, + "aasTemplate": "CIR4FUN.CribCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.heightUnit", + "elementId": "heightUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-crib" + ], + "aliases": [ + "HeightUnit", + "Characteristics.HeightUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "HeightUnit", + "aasPath": "Characteristics.HeightUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "CribCharacteristics.heightUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.2", + "figureNumber": 23, + "aasTemplate": "CIR4FUN.CribCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.diameterUnit", + "elementId": "diameterUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-crib" + ], + "aliases": [ + "DiameterUnit", + "Characteristics.DiameterUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "DiameterUnit", + "aasPath": "Characteristics.DiameterUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "CribCharacteristics.diameterUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.2", + "figureNumber": 23, + "aasTemplate": "CIR4FUN.CribCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.weightUnit", + "elementId": "weightUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "mass-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-crib" + ], + "aliases": [ + "WeightUnit", + "Characteristics.WeightUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "WeightUnit", + "aasPath": "Characteristics.WeightUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "CribCharacteristics.weightUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.2", + "figureNumber": 23, + "aasTemplate": "CIR4FUN.CribCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.volumeUnit", + "elementId": "volumeUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "volume-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-crib" + ], + "aliases": [ + "VolumeUnit", + "Characteristics.VolumeUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "VolumeUnit", + "aasPath": "Characteristics.VolumeUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "CribCharacteristics.volumeUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.2", + "figureNumber": 23, + "aasTemplate": "CIR4FUN.CribCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.payload", + "elementId": "payload", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Payload", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "mass", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-crib" + ], + "aliases": [ + "Payload", + "Characteristics.Payload", + "https://www.cir4fun.eu/ontology/dpp4fun#Payload" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Payload", + "aasPath": "Characteristics.Payload", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "CribCharacteristics.payload", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.2", + "figureNumber": 23, + "aasTemplate": "CIR4FUN.CribCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.internalLength", + "elementId": "internalLength", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#InternalLength", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "lengthUnit", + "appliesTo": [ + "characteristics-crib" + ], + "aliases": [ + "InternalLength", + "Characteristics.InternalLength", + "https://www.cir4fun.eu/ontology/dpp4fun#InternalLength" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "InternalLength", + "aasPath": "Characteristics.InternalLength", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "CribCharacteristics.internalLength", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.2", + "figureNumber": 23, + "aasTemplate": "CIR4FUN.CribCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.internalWidth", + "elementId": "internalWidth", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#InternalWidth", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "widthUnit", + "appliesTo": [ + "characteristics-crib" + ], + "aliases": [ + "InternalWidth", + "Characteristics.InternalWidth", + "https://www.cir4fun.eu/ontology/dpp4fun#InternalWidth" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "InternalWidth", + "aasPath": "Characteristics.InternalWidth", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "CribCharacteristics.internalWidth", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.2", + "figureNumber": 23, + "aasTemplate": "CIR4FUN.CribCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.matelasIncluded", + "elementId": "matelasIncluded", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#MatelasIncluded", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-crib" + ], + "aliases": [ + "MatelasIncluded", + "Characteristics.MatelasIncluded", + "https://www.cir4fun.eu/ontology/dpp4fun#MatelasIncluded", + "mattressIncluded" + ], + "source": { + "aasValueType": "xs:boolean", + "conflictNote": "Published Dpp4Fun spelling MatelasIncluded; do not silently rewrite external semantic IRI", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "MatelasIncluded", + "aasPath": "Characteristics.MatelasIncluded", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "CribCharacteristics.matelasIncluded", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.2", + "figureNumber": 23, + "aasTemplate": "CIR4FUN.CribCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Published Dpp4Fun spelling MatelasIncluded; do not silently rewrite external semantic IRI" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics", + "elementId": "characteristics", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Characteristics", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-fabric" + ], + "aliases": [ + "Characteristics", + "https://www.cir4fun.eu/ontology/dpp4fun#Characteristics" + ], + "source": { + "conflictNote": "product-specific template", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Characteristics", + "aasPath": "Characteristics", + "aasModelType": "Submodel", + "cardinalityProvenance": "EN root elementId uniqueness; D4.1 aspect presence not mandated" + }, + "javaConcept": "FabricCharacteristics", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.3", + "figureNumber": 24, + "aasTemplate": "CIR4FUN.FabricCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "product-specific template" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.length", + "elementId": "length", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Length", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "lengthUnit", + "appliesTo": [ + "characteristics-fabric" + ], + "aliases": [ + "Length", + "Characteristics.Length", + "https://www.cir4fun.eu/ontology/dpp4fun#Length" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Length", + "aasPath": "Characteristics.Length", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "FabricCharacteristics.length", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.3", + "figureNumber": 24, + "aasTemplate": "CIR4FUN.FabricCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.width", + "elementId": "width", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Width", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "widthUnit", + "appliesTo": [ + "characteristics-fabric" + ], + "aliases": [ + "Width", + "Characteristics.Width", + "https://www.cir4fun.eu/ontology/dpp4fun#Width" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Width", + "aasPath": "Characteristics.Width", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "FabricCharacteristics.width", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.3", + "figureNumber": 24, + "aasTemplate": "CIR4FUN.FabricCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.height", + "elementId": "height", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Height", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "heightUnit", + "appliesTo": [ + "characteristics-fabric" + ], + "aliases": [ + "Height", + "Characteristics.Height", + "https://www.cir4fun.eu/ontology/dpp4fun#Height" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Height", + "aasPath": "Characteristics.Height", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "FabricCharacteristics.height", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.3", + "figureNumber": 24, + "aasTemplate": "CIR4FUN.FabricCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.diameter", + "elementId": "diameter", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Diameter", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "diameterUnit", + "appliesTo": [ + "characteristics-fabric" + ], + "aliases": [ + "Diameter", + "Characteristics.Diameter", + "https://www.cir4fun.eu/ontology/dpp4fun#Diameter" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Diameter", + "aasPath": "Characteristics.Diameter", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "FabricCharacteristics.diameter", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.3", + "figureNumber": 24, + "aasTemplate": "CIR4FUN.FabricCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.weight", + "elementId": "weight", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Weight", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "mass", + "unitSlotPath": "weightUnit", + "appliesTo": [ + "characteristics-fabric" + ], + "aliases": [ + "Weight", + "Characteristics.Weight", + "https://www.cir4fun.eu/ontology/dpp4fun#Weight" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Weight", + "aasPath": "Characteristics.Weight", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "FabricCharacteristics.weight", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.3", + "figureNumber": 24, + "aasTemplate": "CIR4FUN.FabricCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.volume", + "elementId": "volume", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Volumn", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "volume", + "unitSlotPath": "volumeUnit", + "appliesTo": [ + "characteristics-fabric" + ], + "aliases": [ + "Volume", + "Characteristics.Volume", + "https://www.cir4fun.eu/ontology/dpp4fun#Volumn" + ], + "source": { + "aasValueType": "xs:double", + "conflictNote": "Published ontology IRI is misspelled #Volumn; keep external IRI, canonical EN elementId is volume", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Volume", + "aasPath": "Characteristics.Volume", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "FabricCharacteristics.volume", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.3", + "figureNumber": 24, + "aasTemplate": "CIR4FUN.FabricCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Published ontology IRI is misspelled #Volumn; keep external IRI, canonical EN elementId is volume" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.surface", + "elementId": "surface", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-fabric" + ], + "aliases": [ + "Surface", + "Characteristics.Surface", + "https://www.cir4fun.eu/ontology/dpp4fun#Surface" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "D4.1 prose describes numeric total area as Double, while its UML, six pinned AAS templates, and populated examples define Surface as a String surface type. Follow the published AAS binding for furniture Characteristics.", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Surface", + "aasPath": "Characteristics.Surface", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED", + "resolutionStatus": "RESOLVED_WITHOUT_DICTIONARY_REFERENCE", + "resolutionNote": "Follow the published AAS template and populated examples for Characteristics Surface: xsd:string describing surface type. D4.1 numeric total-area prose conflicts with its UML and published data; do not invent a SurfaceArea field or semantic IRI." + }, + "javaConcept": "FabricCharacteristics.surface", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.3", + "figureNumber": 24, + "aasTemplate": "CIR4FUN.FabricCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITHOUT_DICTIONARY_REFERENCE", + "conflictResolution": { + "note": "Use xsd:string per the six published AAS templates and populated examples; D4.1 numeric total-area prose conflicts with UML and published data. Keep dictionaryReference null; no semantic IRI is added." + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.color", + "elementId": "color", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Color", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-fabric" + ], + "aliases": [ + "Color", + "Characteristics.Color", + "https://www.cir4fun.eu/ontology/dpp4fun#Color" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "D4.1 prose uses Colour spelling; AAS+ontology publish Color", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Color", + "aasPath": "Characteristics.Color", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "FabricCharacteristics.color", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.3", + "figureNumber": 24, + "aasTemplate": "CIR4FUN.FabricCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "D4.1 prose uses Colour spelling; AAS+ontology publish Color" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.lengthUnit", + "elementId": "lengthUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-fabric" + ], + "aliases": [ + "LengthUnit", + "Characteristics.LengthUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "LengthUnit", + "aasPath": "Characteristics.LengthUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "FabricCharacteristics.lengthUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.3", + "figureNumber": 24, + "aasTemplate": "CIR4FUN.FabricCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.widthUnit", + "elementId": "widthUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-fabric" + ], + "aliases": [ + "WidthUnit", + "Characteristics.WidthUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "WidthUnit", + "aasPath": "Characteristics.WidthUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "FabricCharacteristics.widthUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.3", + "figureNumber": 24, + "aasTemplate": "CIR4FUN.FabricCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.heightUnit", + "elementId": "heightUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-fabric" + ], + "aliases": [ + "HeightUnit", + "Characteristics.HeightUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "HeightUnit", + "aasPath": "Characteristics.HeightUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "FabricCharacteristics.heightUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.3", + "figureNumber": 24, + "aasTemplate": "CIR4FUN.FabricCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.diameterUnit", + "elementId": "diameterUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-fabric" + ], + "aliases": [ + "DiameterUnit", + "Characteristics.DiameterUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "DiameterUnit", + "aasPath": "Characteristics.DiameterUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "FabricCharacteristics.diameterUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.3", + "figureNumber": 24, + "aasTemplate": "CIR4FUN.FabricCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.weightUnit", + "elementId": "weightUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "mass-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-fabric" + ], + "aliases": [ + "WeightUnit", + "Characteristics.WeightUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "WeightUnit", + "aasPath": "Characteristics.WeightUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "FabricCharacteristics.weightUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.3", + "figureNumber": 24, + "aasTemplate": "CIR4FUN.FabricCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.volumeUnit", + "elementId": "volumeUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "volume-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-fabric" + ], + "aliases": [ + "VolumeUnit", + "Characteristics.VolumeUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "VolumeUnit", + "aasPath": "Characteristics.VolumeUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "FabricCharacteristics.volumeUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.3", + "figureNumber": 24, + "aasTemplate": "CIR4FUN.FabricCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.lightFastness", + "elementId": "lightFastness", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#LightFastness", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-fabric" + ], + "aliases": [ + "LightFastness", + "Characteristics.LightFastness", + "https://www.cir4fun.eu/ontology/dpp4fun#LightFastness" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "LightFastness", + "aasPath": "Characteristics.LightFastness", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "FabricCharacteristics.lightFastness", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.3", + "figureNumber": 24, + "aasTemplate": "CIR4FUN.FabricCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.abrasionResistance", + "elementId": "abrasionResistance", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#AbrasionResistant", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-fabric" + ], + "aliases": [ + "AbrasionResistance", + "Characteristics.AbrasionResistance", + "https://www.cir4fun.eu/ontology/dpp4fun#AbrasionResistant" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "AAS/D4.1 noun vs ontology adjective", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "AbrasionResistance", + "aasPath": "Characteristics.AbrasionResistance", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "FabricCharacteristics.abrasionResistance", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.3", + "figureNumber": 24, + "aasTemplate": "CIR4FUN.FabricCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "AAS/D4.1 noun vs ontology adjective" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.pilling", + "elementId": "pilling", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Pilling", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-fabric" + ], + "aliases": [ + "Pilling", + "Characteristics.Pilling", + "https://www.cir4fun.eu/ontology/dpp4fun#Pilling" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Pilling", + "aasPath": "Characteristics.Pilling", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "FabricCharacteristics.pilling", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.3", + "figureNumber": 24, + "aasTemplate": "CIR4FUN.FabricCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.seamSlippage", + "elementId": "seamSlippage", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#SeamSlippage", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-fabric" + ], + "aliases": [ + "SeamSlippage", + "Characteristics.SeamSlippage", + "https://www.cir4fun.eu/ontology/dpp4fun#SeamSlippage" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "SeamSlippage", + "aasPath": "Characteristics.SeamSlippage", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "FabricCharacteristics.seamSlippage", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.3", + "figureNumber": 24, + "aasTemplate": "CIR4FUN.FabricCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.flameRetardant", + "elementId": "flameRetardant", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#FlameRetardant", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-fabric" + ], + "aliases": [ + "FlameRetardant", + "Characteristics.FlameRetardant", + "https://www.cir4fun.eu/ontology/dpp4fun#FlameRetardant" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "D4.1 prose p.63 says Boolean; Figure 24 UML p.62 and pinned AAS say String", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "FlameRetardant", + "aasPath": "Characteristics.FlameRetardant", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED", + "resolutionNote": "D4.1 v1.0 p.63 field description defines Flame Retardant as Boolean; Figure 24 UML p.62 and pinned AAS template use String. Resolved to the D4.1 field description type; source conflict retained." + }, + "javaConcept": "FabricCharacteristics.flameRetardant", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.3", + "figureNumber": 24, + "aasTemplate": "CIR4FUN.FabricCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "D4.1 prose p.63 says Boolean; Figure 24 UML p.62 and pinned AAS say String" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.colorFastness", + "elementId": "colorFastness", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#ColorFastness", + "valueDataType": "xsd:integer", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-fabric" + ], + "aliases": [ + "ColorFastness", + "Characteristics.ColorFastness", + "https://www.cir4fun.eu/ontology/dpp4fun#ColorFastness" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "D4.1 prose p.63 says Integer; Figure 24 UML p.62 and pinned AAS say String", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ColorFastness", + "aasPath": "Characteristics.ColorFastness", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED", + "resolutionNote": "D4.1 v1.0 p.63 field description defines Color Fastness as Integer; Figure 24 UML p.62 and pinned AAS template use String. Resolved to the D4.1 field description type; source conflict retained." + }, + "javaConcept": "FabricCharacteristics.colorFastness", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.3", + "figureNumber": 24, + "aasTemplate": "CIR4FUN.FabricCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "D4.1 prose p.63 says Integer; Figure 24 UML p.62 and pinned AAS say String" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.texture", + "elementId": "texture", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Texture", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-fabric" + ], + "aliases": [ + "Texture", + "Characteristics.Texture", + "https://www.cir4fun.eu/ontology/dpp4fun#Texture" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Texture", + "aasPath": "Characteristics.Texture", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "FabricCharacteristics.texture", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.3", + "figureNumber": 24, + "aasTemplate": "CIR4FUN.FabricCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics", + "elementId": "characteristics", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Characteristics", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-locker" + ], + "aliases": [ + "Characteristics", + "https://www.cir4fun.eu/ontology/dpp4fun#Characteristics" + ], + "source": { + "conflictNote": "product-specific template", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Characteristics", + "aasPath": "Characteristics", + "aasModelType": "Submodel", + "cardinalityProvenance": "EN root elementId uniqueness; D4.1 aspect presence not mandated" + }, + "javaConcept": "LockerCharacteristics", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.4", + "figureNumber": 25, + "aasTemplate": "CIR4FUN.LockerCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "product-specific template" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.length", + "elementId": "length", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Length", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "lengthUnit", + "appliesTo": [ + "characteristics-locker" + ], + "aliases": [ + "Length", + "Characteristics.Length", + "https://www.cir4fun.eu/ontology/dpp4fun#Length" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Length", + "aasPath": "Characteristics.Length", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "LockerCharacteristics.length", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.4", + "figureNumber": 25, + "aasTemplate": "CIR4FUN.LockerCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.width", + "elementId": "width", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Width", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "widthUnit", + "appliesTo": [ + "characteristics-locker" + ], + "aliases": [ + "Width", + "Characteristics.Width", + "https://www.cir4fun.eu/ontology/dpp4fun#Width" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Width", + "aasPath": "Characteristics.Width", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "LockerCharacteristics.width", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.4", + "figureNumber": 25, + "aasTemplate": "CIR4FUN.LockerCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.height", + "elementId": "height", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Height", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "heightUnit", + "appliesTo": [ + "characteristics-locker" + ], + "aliases": [ + "Height", + "Characteristics.Height", + "https://www.cir4fun.eu/ontology/dpp4fun#Height" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Height", + "aasPath": "Characteristics.Height", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "LockerCharacteristics.height", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.4", + "figureNumber": 25, + "aasTemplate": "CIR4FUN.LockerCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.diameter", + "elementId": "diameter", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Diameter", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "diameterUnit", + "appliesTo": [ + "characteristics-locker" + ], + "aliases": [ + "Diameter", + "Characteristics.Diameter", + "https://www.cir4fun.eu/ontology/dpp4fun#Diameter" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Diameter", + "aasPath": "Characteristics.Diameter", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "LockerCharacteristics.diameter", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.4", + "figureNumber": 25, + "aasTemplate": "CIR4FUN.LockerCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.weight", + "elementId": "weight", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Weight", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "mass", + "unitSlotPath": "weightUnit", + "appliesTo": [ + "characteristics-locker" + ], + "aliases": [ + "Weight", + "Characteristics.Weight", + "https://www.cir4fun.eu/ontology/dpp4fun#Weight" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Weight", + "aasPath": "Characteristics.Weight", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "LockerCharacteristics.weight", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.4", + "figureNumber": 25, + "aasTemplate": "CIR4FUN.LockerCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.volume", + "elementId": "volume", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Volumn", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "volume", + "unitSlotPath": "volumeUnit", + "appliesTo": [ + "characteristics-locker" + ], + "aliases": [ + "Volume", + "Characteristics.Volume", + "https://www.cir4fun.eu/ontology/dpp4fun#Volumn" + ], + "source": { + "aasValueType": "xs:double", + "conflictNote": "Published ontology IRI is misspelled #Volumn; keep external IRI, canonical EN elementId is volume", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Volume", + "aasPath": "Characteristics.Volume", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "LockerCharacteristics.volume", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.4", + "figureNumber": 25, + "aasTemplate": "CIR4FUN.LockerCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Published ontology IRI is misspelled #Volumn; keep external IRI, canonical EN elementId is volume" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.surface", + "elementId": "surface", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-locker" + ], + "aliases": [ + "Surface", + "Characteristics.Surface", + "https://www.cir4fun.eu/ontology/dpp4fun#Surface" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "D4.1 prose describes numeric total area as Double, while its UML, six pinned AAS templates, and populated examples define Surface as a String surface type. Follow the published AAS binding for furniture Characteristics.", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Surface", + "aasPath": "Characteristics.Surface", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED", + "resolutionStatus": "RESOLVED_WITHOUT_DICTIONARY_REFERENCE", + "resolutionNote": "Follow the published AAS template and populated examples for Characteristics Surface: xsd:string describing surface type. D4.1 numeric total-area prose conflicts with its UML and published data; do not invent a SurfaceArea field or semantic IRI." + }, + "javaConcept": "LockerCharacteristics.surface", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.4", + "figureNumber": 25, + "aasTemplate": "CIR4FUN.LockerCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITHOUT_DICTIONARY_REFERENCE", + "conflictResolution": { + "note": "Use xsd:string per the six published AAS templates and populated examples; D4.1 numeric total-area prose conflicts with UML and published data. Keep dictionaryReference null; no semantic IRI is added." + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.color", + "elementId": "color", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Color", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-locker" + ], + "aliases": [ + "Color", + "Characteristics.Color", + "https://www.cir4fun.eu/ontology/dpp4fun#Color" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "D4.1 prose uses Colour spelling; AAS+ontology publish Color", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Color", + "aasPath": "Characteristics.Color", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "LockerCharacteristics.color", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.4", + "figureNumber": 25, + "aasTemplate": "CIR4FUN.LockerCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "D4.1 prose uses Colour spelling; AAS+ontology publish Color" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.lengthUnit", + "elementId": "lengthUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-locker" + ], + "aliases": [ + "LengthUnit", + "Characteristics.LengthUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "LengthUnit", + "aasPath": "Characteristics.LengthUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "LockerCharacteristics.lengthUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.4", + "figureNumber": 25, + "aasTemplate": "CIR4FUN.LockerCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.widthUnit", + "elementId": "widthUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-locker" + ], + "aliases": [ + "WidthUnit", + "Characteristics.WidthUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "WidthUnit", + "aasPath": "Characteristics.WidthUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "LockerCharacteristics.widthUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.4", + "figureNumber": 25, + "aasTemplate": "CIR4FUN.LockerCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.heightUnit", + "elementId": "heightUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-locker" + ], + "aliases": [ + "HeightUnit", + "Characteristics.HeightUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "HeightUnit", + "aasPath": "Characteristics.HeightUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "LockerCharacteristics.heightUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.4", + "figureNumber": 25, + "aasTemplate": "CIR4FUN.LockerCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.diameterUnit", + "elementId": "diameterUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-locker" + ], + "aliases": [ + "DiameterUnit", + "Characteristics.DiameterUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "DiameterUnit", + "aasPath": "Characteristics.DiameterUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "LockerCharacteristics.diameterUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.4", + "figureNumber": 25, + "aasTemplate": "CIR4FUN.LockerCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.weightUnit", + "elementId": "weightUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "mass-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-locker" + ], + "aliases": [ + "WeightUnit", + "Characteristics.WeightUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "WeightUnit", + "aasPath": "Characteristics.WeightUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "LockerCharacteristics.weightUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.4", + "figureNumber": 25, + "aasTemplate": "CIR4FUN.LockerCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.volumeUnit", + "elementId": "volumeUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "volume-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-locker" + ], + "aliases": [ + "VolumeUnit", + "Characteristics.VolumeUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "VolumeUnit", + "aasPath": "Characteristics.VolumeUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "LockerCharacteristics.volumeUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.4", + "figureNumber": 25, + "aasTemplate": "CIR4FUN.LockerCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.valvePayload", + "elementId": "valvePayload", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#ValvePayload", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "mass", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-locker" + ], + "aliases": [ + "ValvePayload", + "Characteristics.ValvePayload", + "https://www.cir4fun.eu/ontology/dpp4fun#ValvePayload" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ValvePayload", + "aasPath": "Characteristics.ValvePayload", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "LockerCharacteristics.valvePayload", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.4", + "figureNumber": 25, + "aasTemplate": "CIR4FUN.LockerCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.wallThickness", + "elementId": "wallThickness", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#WallThickness", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-locker" + ], + "aliases": [ + "WallThickness", + "Characteristics.WallThickness", + "https://www.cir4fun.eu/ontology/dpp4fun#WallThickness" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "WallThickness", + "aasPath": "Characteristics.WallThickness", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "LockerCharacteristics.wallThickness", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.4", + "figureNumber": 25, + "aasTemplate": "CIR4FUN.LockerCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.lockIncluded", + "elementId": "lockIncluded", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#LockIncluded", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-locker" + ], + "aliases": [ + "LockIncluded", + "Characteristics.LockIncluded", + "https://www.cir4fun.eu/ontology/dpp4fun#LockIncluded" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "LockIncluded", + "aasPath": "Characteristics.LockIncluded", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "LockerCharacteristics.lockIncluded", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.4", + "figureNumber": 25, + "aasTemplate": "CIR4FUN.LockerCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics", + "elementId": "characteristics", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Characteristics", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-particleBoard" + ], + "aliases": [ + "Characteristics", + "https://www.cir4fun.eu/ontology/dpp4fun#Characteristics" + ], + "source": { + "conflictNote": "product-specific template", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Characteristics", + "aasPath": "Characteristics", + "aasModelType": "Submodel", + "cardinalityProvenance": "EN root elementId uniqueness; D4.1 aspect presence not mandated" + }, + "javaConcept": "ParticleBoardCharacteristics", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.5", + "figureNumber": 26, + "aasTemplate": "CIR4FUN.ParticleBoardCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "product-specific template" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.length", + "elementId": "length", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Length", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "lengthUnit", + "appliesTo": [ + "characteristics-particleBoard" + ], + "aliases": [ + "Length", + "Characteristics.Length", + "https://www.cir4fun.eu/ontology/dpp4fun#Length" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Length", + "aasPath": "Characteristics.Length", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ParticleBoardCharacteristics.length", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.5", + "figureNumber": 26, + "aasTemplate": "CIR4FUN.ParticleBoardCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.width", + "elementId": "width", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Width", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "widthUnit", + "appliesTo": [ + "characteristics-particleBoard" + ], + "aliases": [ + "Width", + "Characteristics.Width", + "https://www.cir4fun.eu/ontology/dpp4fun#Width" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Width", + "aasPath": "Characteristics.Width", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ParticleBoardCharacteristics.width", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.5", + "figureNumber": 26, + "aasTemplate": "CIR4FUN.ParticleBoardCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.height", + "elementId": "height", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Height", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "heightUnit", + "appliesTo": [ + "characteristics-particleBoard" + ], + "aliases": [ + "Height", + "Characteristics.Height", + "https://www.cir4fun.eu/ontology/dpp4fun#Height" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Height", + "aasPath": "Characteristics.Height", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ParticleBoardCharacteristics.height", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.5", + "figureNumber": 26, + "aasTemplate": "CIR4FUN.ParticleBoardCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.diameter", + "elementId": "diameter", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Diameter", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "diameterUnit", + "appliesTo": [ + "characteristics-particleBoard" + ], + "aliases": [ + "Diameter", + "Characteristics.Diameter", + "https://www.cir4fun.eu/ontology/dpp4fun#Diameter" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Diameter", + "aasPath": "Characteristics.Diameter", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ParticleBoardCharacteristics.diameter", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.5", + "figureNumber": 26, + "aasTemplate": "CIR4FUN.ParticleBoardCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.weight", + "elementId": "weight", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Weight", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "mass", + "unitSlotPath": "weightUnit", + "appliesTo": [ + "characteristics-particleBoard" + ], + "aliases": [ + "Weight", + "Characteristics.Weight", + "https://www.cir4fun.eu/ontology/dpp4fun#Weight" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Weight", + "aasPath": "Characteristics.Weight", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ParticleBoardCharacteristics.weight", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.5", + "figureNumber": 26, + "aasTemplate": "CIR4FUN.ParticleBoardCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.volume", + "elementId": "volume", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Volumn", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "volume", + "unitSlotPath": "volumeUnit", + "appliesTo": [ + "characteristics-particleBoard" + ], + "aliases": [ + "Volume", + "Characteristics.Volume", + "https://www.cir4fun.eu/ontology/dpp4fun#Volumn" + ], + "source": { + "aasValueType": "xs:double", + "conflictNote": "Published ontology IRI is misspelled #Volumn; keep external IRI, canonical EN elementId is volume", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Volume", + "aasPath": "Characteristics.Volume", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ParticleBoardCharacteristics.volume", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.5", + "figureNumber": 26, + "aasTemplate": "CIR4FUN.ParticleBoardCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Published ontology IRI is misspelled #Volumn; keep external IRI, canonical EN elementId is volume" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.surface", + "elementId": "surface", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-particleBoard" + ], + "aliases": [ + "Surface", + "Characteristics.Surface", + "https://www.cir4fun.eu/ontology/dpp4fun#Surface" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "D4.1 prose describes numeric total area as Double, while its UML, six pinned AAS templates, and populated examples define Surface as a String surface type. Follow the published AAS binding for furniture Characteristics.", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Surface", + "aasPath": "Characteristics.Surface", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED", + "resolutionStatus": "RESOLVED_WITHOUT_DICTIONARY_REFERENCE", + "resolutionNote": "Follow the published AAS template and populated examples for Characteristics Surface: xsd:string describing surface type. D4.1 numeric total-area prose conflicts with its UML and published data; do not invent a SurfaceArea field or semantic IRI." + }, + "javaConcept": "ParticleBoardCharacteristics.surface", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.5", + "figureNumber": 26, + "aasTemplate": "CIR4FUN.ParticleBoardCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITHOUT_DICTIONARY_REFERENCE", + "conflictResolution": { + "note": "Use xsd:string per the six published AAS templates and populated examples; D4.1 numeric total-area prose conflicts with UML and published data. Keep dictionaryReference null; no semantic IRI is added." + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.color", + "elementId": "color", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Color", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-particleBoard" + ], + "aliases": [ + "Color", + "Characteristics.Color", + "https://www.cir4fun.eu/ontology/dpp4fun#Color" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "D4.1 prose uses Colour spelling; AAS+ontology publish Color", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Color", + "aasPath": "Characteristics.Color", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ParticleBoardCharacteristics.color", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.5", + "figureNumber": 26, + "aasTemplate": "CIR4FUN.ParticleBoardCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "D4.1 prose uses Colour spelling; AAS+ontology publish Color" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.lengthUnit", + "elementId": "lengthUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-particleBoard" + ], + "aliases": [ + "LengthUnit", + "Characteristics.LengthUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "LengthUnit", + "aasPath": "Characteristics.LengthUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ParticleBoardCharacteristics.lengthUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.5", + "figureNumber": 26, + "aasTemplate": "CIR4FUN.ParticleBoardCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.widthUnit", + "elementId": "widthUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-particleBoard" + ], + "aliases": [ + "WidthUnit", + "Characteristics.WidthUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "WidthUnit", + "aasPath": "Characteristics.WidthUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ParticleBoardCharacteristics.widthUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.5", + "figureNumber": 26, + "aasTemplate": "CIR4FUN.ParticleBoardCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.heightUnit", + "elementId": "heightUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-particleBoard" + ], + "aliases": [ + "HeightUnit", + "Characteristics.HeightUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "HeightUnit", + "aasPath": "Characteristics.HeightUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ParticleBoardCharacteristics.heightUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.5", + "figureNumber": 26, + "aasTemplate": "CIR4FUN.ParticleBoardCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.diameterUnit", + "elementId": "diameterUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-particleBoard" + ], + "aliases": [ + "DiameterUnit", + "Characteristics.DiameterUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "DiameterUnit", + "aasPath": "Characteristics.DiameterUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ParticleBoardCharacteristics.diameterUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.5", + "figureNumber": 26, + "aasTemplate": "CIR4FUN.ParticleBoardCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.weightUnit", + "elementId": "weightUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "mass-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-particleBoard" + ], + "aliases": [ + "WeightUnit", + "Characteristics.WeightUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "WeightUnit", + "aasPath": "Characteristics.WeightUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ParticleBoardCharacteristics.weightUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.5", + "figureNumber": 26, + "aasTemplate": "CIR4FUN.ParticleBoardCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.volumeUnit", + "elementId": "volumeUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "volume-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-particleBoard" + ], + "aliases": [ + "VolumeUnit", + "Characteristics.VolumeUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "VolumeUnit", + "aasPath": "Characteristics.VolumeUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ParticleBoardCharacteristics.volumeUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.5", + "figureNumber": 26, + "aasTemplate": "CIR4FUN.ParticleBoardCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.texture", + "elementId": "texture", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Texture", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-particleBoard" + ], + "aliases": [ + "Texture", + "Characteristics.Texture", + "https://www.cir4fun.eu/ontology/dpp4fun#Texture" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Texture", + "aasPath": "Characteristics.Texture", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ParticleBoardCharacteristics.texture", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.5", + "figureNumber": 26, + "aasTemplate": "CIR4FUN.ParticleBoardCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.easyToClean", + "elementId": "easyToClean", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#EasyToClean", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-particleBoard" + ], + "aliases": [ + "EasyToClean", + "Characteristics.EasyToClean", + "https://www.cir4fun.eu/ontology/dpp4fun#EasyToClean" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "EasyToClean", + "aasPath": "Characteristics.EasyToClean", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ParticleBoardCharacteristics.easyToClean", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.5", + "figureNumber": 26, + "aasTemplate": "CIR4FUN.ParticleBoardCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.easyToMill", + "elementId": "easyToMill", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#EasyToMill", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-particleBoard" + ], + "aliases": [ + "EasyToMill", + "Characteristics.EasyToMill", + "https://www.cir4fun.eu/ontology/dpp4fun#EasyToMill" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "EasyToMill", + "aasPath": "Characteristics.EasyToMill", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ParticleBoardCharacteristics.easyToMill", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.5", + "figureNumber": 26, + "aasTemplate": "CIR4FUN.ParticleBoardCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.stainResistant", + "elementId": "stainResistant", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#StainResistant", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-particleBoard" + ], + "aliases": [ + "StainResistant", + "Characteristics.StainResistant", + "https://www.cir4fun.eu/ontology/dpp4fun#StainResistant" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "StainResistant", + "aasPath": "Characteristics.StainResistant", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ParticleBoardCharacteristics.stainResistant", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.5", + "figureNumber": 26, + "aasTemplate": "CIR4FUN.ParticleBoardCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.scratchResistant", + "elementId": "scratchResistant", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#ScratchResistant", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-particleBoard" + ], + "aliases": [ + "ScratchResistant", + "Characteristics.ScratchResistant", + "https://www.cir4fun.eu/ontology/dpp4fun#ScratchResistant" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ScratchResistant", + "aasPath": "Characteristics.ScratchResistant", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ParticleBoardCharacteristics.scratchResistant", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.5", + "figureNumber": 26, + "aasTemplate": "CIR4FUN.ParticleBoardCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.antiBacterial", + "elementId": "antiBacterial", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#AntiBacterial", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-particleBoard" + ], + "aliases": [ + "AntiBacterial", + "Characteristics.AntiBacterial", + "https://www.cir4fun.eu/ontology/dpp4fun#AntiBacterial" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "AntiBacterial", + "aasPath": "Characteristics.AntiBacterial", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ParticleBoardCharacteristics.antiBacterial", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.5", + "figureNumber": 26, + "aasTemplate": "CIR4FUN.ParticleBoardCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.carb2Epa", + "elementId": "carb2Epa", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#CARB2-EPA", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-particleBoard" + ], + "aliases": [ + "CARB2EPA", + "Characteristics.CARB2EPA", + "https://www.cir4fun.eu/ontology/dpp4fun#CARB2-EPA" + ], + "source": { + "aasValueType": "xs:boolean", + "conflictNote": "AAS idShort CARB2EPA vs ontology #CARB2-EPA", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "CARB2EPA", + "aasPath": "Characteristics.CARB2EPA", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "ParticleBoardCharacteristics.carb2Epa", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.5", + "figureNumber": 26, + "aasTemplate": "CIR4FUN.ParticleBoardCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "AAS idShort CARB2EPA vs ontology #CARB2-EPA" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics", + "elementId": "characteristics", + "objectType": "DataElementCollection", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Characteristics", + "valueDataType": null, + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "Characteristics", + "https://www.cir4fun.eu/ontology/dpp4fun#Characteristics" + ], + "source": { + "conflictNote": "product-specific template", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Characteristics", + "aasPath": "Characteristics", + "aasModelType": "Submodel", + "cardinalityProvenance": "EN root elementId uniqueness; D4.1 aspect presence not mandated" + }, + "javaConcept": "SofaCharacteristics", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "product-specific template" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.length", + "elementId": "length", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Length", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "lengthUnit", + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "Length", + "Characteristics.Length", + "https://www.cir4fun.eu/ontology/dpp4fun#Length" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Length", + "aasPath": "Characteristics.Length", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.length", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.width", + "elementId": "width", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Width", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "widthUnit", + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "Width", + "Characteristics.Width", + "https://www.cir4fun.eu/ontology/dpp4fun#Width" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Width", + "aasPath": "Characteristics.Width", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.width", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.height", + "elementId": "height", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Height", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "heightUnit", + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "Height", + "Characteristics.Height", + "https://www.cir4fun.eu/ontology/dpp4fun#Height" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Height", + "aasPath": "Characteristics.Height", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.height", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.diameter", + "elementId": "diameter", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Diameter", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "diameterUnit", + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "Diameter", + "Characteristics.Diameter", + "https://www.cir4fun.eu/ontology/dpp4fun#Diameter" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Diameter", + "aasPath": "Characteristics.Diameter", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.diameter", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.weight", + "elementId": "weight", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Weight", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "mass", + "unitSlotPath": "weightUnit", + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "Weight", + "Characteristics.Weight", + "https://www.cir4fun.eu/ontology/dpp4fun#Weight" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Weight", + "aasPath": "Characteristics.Weight", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.weight", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.volume", + "elementId": "volume", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Volumn", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "volume", + "unitSlotPath": "volumeUnit", + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "Volume", + "Characteristics.Volume", + "https://www.cir4fun.eu/ontology/dpp4fun#Volumn" + ], + "source": { + "aasValueType": "xs:double", + "conflictNote": "Published ontology IRI is misspelled #Volumn; keep external IRI, canonical EN elementId is volume", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Volume", + "aasPath": "Characteristics.Volume", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.volume", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "Published ontology IRI is misspelled #Volumn; keep external IRI, canonical EN elementId is volume" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.surface", + "elementId": "surface", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "Surface", + "Characteristics.Surface", + "https://www.cir4fun.eu/ontology/dpp4fun#Surface" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "D4.1 prose describes numeric total area as Double, while its UML, six pinned AAS templates, and populated examples define Surface as a String surface type. Follow the published AAS binding for furniture Characteristics.", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Surface", + "aasPath": "Characteristics.Surface", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED", + "resolutionStatus": "RESOLVED_WITHOUT_DICTIONARY_REFERENCE", + "resolutionNote": "Follow the published AAS template and populated examples for Characteristics Surface: xsd:string describing surface type. D4.1 numeric total-area prose conflicts with its UML and published data; do not invent a SurfaceArea field or semantic IRI." + }, + "javaConcept": "SofaCharacteristics.surface", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITHOUT_DICTIONARY_REFERENCE", + "conflictResolution": { + "note": "Use xsd:string per the six published AAS templates and populated examples; D4.1 numeric total-area prose conflicts with UML and published data. Keep dictionaryReference null; no semantic IRI is added." + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.color", + "elementId": "color", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Color", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "Color", + "Characteristics.Color", + "https://www.cir4fun.eu/ontology/dpp4fun#Color" + ], + "source": { + "aasValueType": "xs:string", + "conflictNote": "D4.1 prose uses Colour spelling; AAS+ontology publish Color", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Color", + "aasPath": "Characteristics.Color", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.color", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "D4.1 prose uses Colour spelling; AAS+ontology publish Color" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.lengthUnit", + "elementId": "lengthUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "LengthUnit", + "Characteristics.LengthUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "LengthUnit", + "aasPath": "Characteristics.LengthUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.lengthUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.widthUnit", + "elementId": "widthUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "WidthUnit", + "Characteristics.WidthUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "WidthUnit", + "aasPath": "Characteristics.WidthUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.widthUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.heightUnit", + "elementId": "heightUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "HeightUnit", + "Characteristics.HeightUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "HeightUnit", + "aasPath": "Characteristics.HeightUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.heightUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.diameterUnit", + "elementId": "diameterUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "DiameterUnit", + "Characteristics.DiameterUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "DiameterUnit", + "aasPath": "Characteristics.DiameterUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.diameterUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.weightUnit", + "elementId": "weightUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "mass-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "WeightUnit", + "Characteristics.WeightUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "WeightUnit", + "aasPath": "Characteristics.WeightUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.weightUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.volumeUnit", + "elementId": "volumeUnit", + "objectType": "SingleValuedDataElement", + "dictionaryReference": null, + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "volume-unit", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "VolumeUnit", + "Characteristics.VolumeUnit" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "VolumeUnit", + "aasPath": "Characteristics.VolumeUnit", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.volumeUnit", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.seatHeight", + "elementId": "seatHeight", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#SeatHeight", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "heightUnit", + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "SeatHeight", + "Characteristics.SeatHeight", + "https://www.cir4fun.eu/ontology/dpp4fun#SeatHeight" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "SeatHeight", + "aasPath": "Characteristics.SeatHeight", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.seatHeight", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.seatWidth", + "elementId": "seatWidth", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#SeatWidth", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "widthUnit", + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "SeatWidth", + "Characteristics.SeatWidth", + "https://www.cir4fun.eu/ontology/dpp4fun#SeatWidth" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "SeatWidth", + "aasPath": "Characteristics.SeatWidth", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.seatWidth", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.seatLength", + "elementId": "seatLength", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#SeatLength", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "lengthUnit", + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "SeatLength", + "Characteristics.SeatLength", + "https://www.cir4fun.eu/ontology/dpp4fun#SeatLength" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "SeatLength", + "aasPath": "Characteristics.SeatLength", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.seatLength", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.armrestHeight", + "elementId": "armrestHeight", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#ArmrestHeight", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "heightUnit", + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "ArmrestHeight", + "Characteristics.ArmrestHeight", + "https://www.cir4fun.eu/ontology/dpp4fun#ArmrestHeight" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ArmrestHeight", + "aasPath": "Characteristics.ArmrestHeight", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.armrestHeight", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.armrestWidth", + "elementId": "armrestWidth", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#ArmrestWidth", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "widthUnit", + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "ArmrestWidth", + "Characteristics.ArmrestWidth", + "https://www.cir4fun.eu/ontology/dpp4fun#ArmrestWidth" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ArmrestWidth", + "aasPath": "Characteristics.ArmrestWidth", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.armrestWidth", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.backrestHeight", + "elementId": "backrestHeight", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#BackrestHeight", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "heightUnit", + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "BackrestHeight", + "Characteristics.BackrestHeight", + "https://www.cir4fun.eu/ontology/dpp4fun#BackrestHeight" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "BackrestHeight", + "aasPath": "Characteristics.BackrestHeight", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.backrestHeight", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.legHeight", + "elementId": "legHeight", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#LegHeight", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "length", + "unitSlotPath": "heightUnit", + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "LegHeight", + "Characteristics.LegHeight", + "https://www.cir4fun.eu/ontology/dpp4fun#LegHeight" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "LegHeight", + "aasPath": "Characteristics.LegHeight", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.legHeight", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.foamHardnessLevel", + "elementId": "foamHardnessLevel", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#FoamHardnessLevel", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "FoamHardnessLevel", + "Characteristics.FoamHardnessLevel", + "https://www.cir4fun.eu/ontology/dpp4fun#FoamHardnessLevel" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "FoamHardnessLevel", + "aasPath": "Characteristics.FoamHardnessLevel", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.foamHardnessLevel", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.seatingCapacity", + "elementId": "seatingCapacity", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#SeatingCapacity", + "valueDataType": "xsd:integer", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "SeatingCapacity", + "Characteristics.SeatingCapacity", + "https://www.cir4fun.eu/ontology/dpp4fun#SeatingCapacity" + ], + "source": { + "aasValueType": "xs:integer", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "SeatingCapacity", + "aasPath": "Characteristics.SeatingCapacity", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.seatingCapacity", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.indoorUse", + "elementId": "indoorUse", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#IndoorUse", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "IndoorUse", + "Characteristics.IndoorUse", + "https://www.cir4fun.eu/ontology/dpp4fun#IndoorUse" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "IndoorUse", + "aasPath": "Characteristics.IndoorUse", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.indoorUse", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.outdoorUse", + "elementId": "outdoorUse", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#OutdoorUse", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "OutdoorUse", + "Characteristics.OutdoorUse", + "https://www.cir4fun.eu/ontology/dpp4fun#OutdoorUse" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "OutdoorUse", + "aasPath": "Characteristics.OutdoorUse", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.outdoorUse", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.reclining", + "elementId": "reclining", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#RecliningSystem", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "Reclining", + "Characteristics.Reclining", + "https://www.cir4fun.eu/ontology/dpp4fun#RecliningSystem" + ], + "source": { + "aasValueType": "xs:boolean", + "conflictNote": "AAS name Reclining; ontology term is #RecliningSystem", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Reclining", + "aasPath": "Characteristics.Reclining", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.reclining", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "AAS name Reclining; ontology term is #RecliningSystem" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.seatSliding", + "elementId": "seatSliding", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#SeatSliding", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "SeatSliding", + "Characteristics.SeatSliding", + "https://www.cir4fun.eu/ontology/dpp4fun#SeatSliding" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "SeatSliding", + "aasPath": "Characteristics.SeatSliding", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.seatSliding", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.swivel", + "elementId": "swivel", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Swivel", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "Swivel", + "Characteristics.Swivel", + "https://www.cir4fun.eu/ontology/dpp4fun#Swivel" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Swivel", + "aasPath": "Characteristics.Swivel", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.swivel", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.waterRepellent", + "elementId": "waterRepellent", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#WaterRepellent", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "WaterRepellent", + "Characteristics.WaterRepellent", + "https://www.cir4fun.eu/ontology/dpp4fun#WaterRepellent" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "WaterRepellent", + "aasPath": "Characteristics.WaterRepellent", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.waterRepellent", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.stainResistant", + "elementId": "stainResistant", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#StainResistant", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "StainResistant", + "Characteristics.StainResistant", + "https://www.cir4fun.eu/ontology/dpp4fun#StainResistant" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "StainResistant", + "aasPath": "Characteristics.StainResistant", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.stainResistant", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.easyToClean", + "elementId": "easyToClean", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#EasyToClean", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "EasyToClean", + "Characteristics.EasyToClean", + "https://www.cir4fun.eu/ontology/dpp4fun#EasyToClean" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "EasyToClean", + "aasPath": "Characteristics.EasyToClean", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.easyToClean", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.fireResistant", + "elementId": "fireResistant", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#FireResistant", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "FireResistant", + "Characteristics.FireResistant", + "https://www.cir4fun.eu/ontology/dpp4fun#FireResistant" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "FireResistant", + "aasPath": "Characteristics.FireResistant", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.fireResistant", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.seatRemovableCover", + "elementId": "seatRemovableCover", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#SeatCoverRemovable", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "SeatRemovableCover", + "Characteristics.SeatRemovableCover", + "https://www.cir4fun.eu/ontology/dpp4fun#SeatCoverRemovable" + ], + "source": { + "aasValueType": "xs:boolean", + "conflictNote": "AAS/D4.1 name vs ontology alias", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "SeatRemovableCover", + "aasPath": "Characteristics.SeatRemovableCover", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.seatRemovableCover", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "AAS/D4.1 name vs ontology alias" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.backrestRemovableCover", + "elementId": "backrestRemovableCover", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#BackrestCoverRemovable", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "BackrestRemovableCover", + "Characteristics.BackrestRemovableCover", + "https://www.cir4fun.eu/ontology/dpp4fun#BackrestCoverRemovable" + ], + "source": { + "aasValueType": "xs:boolean", + "conflictNote": "AAS/D4.1 name vs ontology alias", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "BackrestRemovableCover", + "aasPath": "Characteristics.BackrestRemovableCover", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.backrestRemovableCover", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "AAS/D4.1 name vs ontology alias" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.adjustableFeet", + "elementId": "adjustableFeet", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#FeetAdjustable", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "AdjustableFeet", + "Characteristics.AdjustableFeet", + "https://www.cir4fun.eu/ontology/dpp4fun#FeetAdjustable" + ], + "source": { + "aasValueType": "xs:boolean", + "conflictNote": "AAS/D4.1 name vs ontology alias", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "AdjustableFeet", + "aasPath": "Characteristics.AdjustableFeet", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.adjustableFeet", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "AAS/D4.1 name vs ontology alias" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.armrestIncluded", + "elementId": "armrestIncluded", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#ArmrestIncluded", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "ArmrestIncluded", + "Characteristics.ArmrestIncluded", + "https://www.cir4fun.eu/ontology/dpp4fun#ArmrestIncluded" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ArmrestIncluded", + "aasPath": "Characteristics.ArmrestIncluded", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.armrestIncluded", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.armrestRemovableCover", + "elementId": "armrestRemovableCover", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#ArmrestCoverRemovable", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "ArmrestRemovableCover", + "Characteristics.ArmrestRemovableCover", + "https://www.cir4fun.eu/ontology/dpp4fun#ArmrestCoverRemovable" + ], + "source": { + "aasValueType": "xs:boolean", + "conflictNote": "AAS/D4.1 name vs ontology alias", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ArmrestRemovableCover", + "aasPath": "Characteristics.ArmrestRemovableCover", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.armrestRemovableCover", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "AAS/D4.1 name vs ontology alias" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.payload", + "elementId": "payload", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#Payload", + "valueDataType": "xsd:double", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": "mass", + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "Payload", + "Characteristics.Payload", + "https://www.cir4fun.eu/ontology/dpp4fun#Payload" + ], + "source": { + "aasValueType": "xs:double", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "Payload", + "aasPath": "Characteristics.Payload", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.payload", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.headrestIncluded", + "elementId": "headrestIncluded", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#HeadrestIncluded", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "HeadrestIncluded", + "Characteristics.HeadrestIncluded", + "https://www.cir4fun.eu/ontology/dpp4fun#HeadrestIncluded" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "HeadrestIncluded", + "aasPath": "Characteristics.HeadrestIncluded", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.headrestIncluded", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.additionalCushionIncluded", + "elementId": "additionalCushionIncluded", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#AdditionalCushionIncluded", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "AdditionalCushionIncluded", + "Characteristics.AdditionalCushionIncluded", + "https://www.cir4fun.eu/ontology/dpp4fun#AdditionalCushionIncluded" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "AdditionalCushionIncluded", + "aasPath": "Characteristics.AdditionalCushionIncluded", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.additionalCushionIncluded", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.cushionRemovableCover", + "elementId": "cushionRemovableCover", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#CushionCoverRemovable", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "CushionRemovableCover", + "Characteristics.CushionRemovableCover", + "https://www.cir4fun.eu/ontology/dpp4fun#CushionCoverRemovable" + ], + "source": { + "aasValueType": "xs:boolean", + "conflictNote": "AAS/D4.1 name vs ontology alias", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "CushionRemovableCover", + "aasPath": "Characteristics.CushionRemovableCover", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.cushionRemovableCover", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED_WITH_SOURCE_CONFLICT", + "conflictResolution": { + "note": "AAS/D4.1 name vs ontology alias" + }, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.footrestIncluded", + "elementId": "footrestIncluded", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#FootrestIncluded", + "valueDataType": "xsd:boolean", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "FootrestIncluded", + "Characteristics.FootrestIncluded", + "https://www.cir4fun.eu/ontology/dpp4fun#FootrestIncluded" + ], + "source": { + "aasValueType": "xs:boolean", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "FootrestIncluded", + "aasPath": "Characteristics.FootrestIncluded", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.footrestIncluded", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.armrestType", + "elementId": "armrestType", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#ArmrestType", + "valueDataType": "xsd:string", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "ArmrestType", + "Characteristics.ArmrestType", + "https://www.cir4fun.eu/ontology/dpp4fun#ArmrestType" + ], + "source": { + "aasValueType": "xs:string", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "ArmrestType", + "aasPath": "Characteristics.ArmrestType", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.armrestType", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + }, + { + "path": "characteristics.numberOfCushions", + "elementId": "numberOfCushions", + "objectType": "SingleValuedDataElement", + "dictionaryReference": "https://www.cir4fun.eu/ontology/dpp4fun#NumberOfCushions", + "valueDataType": "xsd:integer", + "sourceCardinality": null, + "bindingOccurrence": { + "min": 0, + "max": 1 + }, + "unitDimension": null, + "unitSlotPath": null, + "appliesTo": [ + "characteristics-sofa" + ], + "aliases": [ + "NumberOfCushions", + "Characteristics.NumberOfCushions", + "https://www.cir4fun.eu/ontology/dpp4fun#NumberOfCushions" + ], + "source": { + "aasValueType": "xs:integer", + "dpp4funCommit": "2007a574b0e35eae5997c9460bf4cf531588405b", + "d4Name": "NumberOfCushions", + "aasPath": "Characteristics.NumberOfCushions", + "aasModelType": "Property", + "cardinalityProvenance": "UNSPECIFIED" + }, + "javaConcept": "SofaCharacteristics.numberOfCushions", + "sourceReferences": [ + { + "source": "D4.1 v1.0", + "section": "3.2.2.6", + "figureNumber": 27, + "aasTemplate": "CIR4FUN.SofaCharacteristics.json" + } + ], + "validationRules": [ + "DATA_ELEMENT_KIND_MISMATCH", + "VALUE_DATA_TYPE_MISMATCH", + "PRODUCT_VARIANT_NOT_APPLICABLE", + "DICTIONARY_REFERENCE_MISMATCH" + ], + "repeatedItemPolicy": null, + "resolutionStatus": "RESOLVED", + "conflictResolution": null, + "explicitRequiredness": "UNSPECIFIED" + } + ], + "semanticRules": [ + { + "id": "DPP4FUN_INSTRUCTION_NAME_REQUIRED", + "pathSuffix": "InstructionSet[*]", + "requiredField": "name", + "requiredAnyFields": [ + + ], + "excludedPaths": [ + + ] + }, + { + "id": "DPP4FUN_INSTRUCTION_DESCRIPTION_REQUIRED", + "pathSuffix": "InstructionSet[*]", + "requiredField": "description", + "requiredAnyFields": [ + + ], + "excludedPaths": [ + + ] + }, + { + "id": "DPP4FUN_INSTRUCTION_REFERENCE_REQUIRED", + "pathSuffix": "InstructionSet[*]", + "requiredField": "reference", + "requiredAnyFields": [ + + ], + "excludedPaths": [ + + ] + }, + { + "id": "DPP4FUN_ASSESSMENT_NAME_REQUIRED", + "pathSuffix": "certificateSet[*]", + "requiredField": "name", + "requiredAnyFields": [ + + ], + "excludedPaths": [ + + ] + }, + { + "id": "DPP4FUN_ASSESSMENT_DESCRIPTION_REQUIRED", + "pathSuffix": "certificateSet[*]", + "requiredField": "description", + "requiredAnyFields": [ + + ], + "excludedPaths": [ + + ] + }, + { + "id": "DPP4FUN_ASSESSMENT_NAME_REQUIRED", + "pathSuffix": "standardSet[*]", + "requiredField": "name", + "requiredAnyFields": [ + + ], + "excludedPaths": [ + + ] + }, + { + "id": "DPP4FUN_ASSESSMENT_DESCRIPTION_REQUIRED", + "pathSuffix": "standardSet[*]", + "requiredField": "description", + "requiredAnyFields": [ + + ], + "excludedPaths": [ + + ] + }, + { + "id": "DPP4FUN_ASSESSMENT_NAME_REQUIRED", + "pathSuffix": "scoringSet[*]", + "requiredField": "name", + "requiredAnyFields": [ + + ], + "excludedPaths": [ + + ] + }, + { + "id": "DPP4FUN_ASSESSMENT_DESCRIPTION_REQUIRED", + "pathSuffix": "scoringSet[*]", + "requiredField": "description", + "requiredAnyFields": [ + + ], + "excludedPaths": [ + + ] + }, + { + "id": "DPP4FUN_ASSESSMENT_NAME_REQUIRED", + "pathSuffix": "indicatorSet[*]", + "requiredField": "name", + "requiredAnyFields": [ + + ], + "excludedPaths": [ + + ] + }, + { + "id": "DPP4FUN_ASSESSMENT_DESCRIPTION_REQUIRED", + "pathSuffix": "indicatorSet[*]", + "requiredField": "description", + "requiredAnyFields": [ + + ], + "excludedPaths": [ + + ] + }, + { + "id": "DPP4FUN_RECORD_NAME_REQUIRED", + "pathSuffix": "RecordSet[*]", + "requiredField": "name", + "requiredAnyFields": [ + + ], + "excludedPaths": [ + + ] + }, + { + "id": "DPP4FUN_RECORD_DESCRIPTION_REQUIRED", + "pathSuffix": "RecordSet[*]", + "requiredField": "description", + "requiredAnyFields": [ + + ], + "excludedPaths": [ + + ] + }, + { + "id": "DPP4FUN_RECORD_OPERATOR_REQUIRED", + "pathSuffix": "RecordSet[*]", + "requiredField": "operator", + "requiredAnyFields": [ + + ], + "excludedPaths": [ + + ] + }, + { + "id": "DPP4FUN_RECORD_TIME_REQUIRED", + "pathSuffix": "RecordSet[*]", + "requiredField": null, + "requiredAnyFields": [ + "time", + "startTime", + "endTime" + ], + "excludedPaths": [ + + ] + }, + { + "id": "DPP4FUN_RECORD_STATUS_REQUIRED", + "pathSuffix": "RecordSet[*]", + "requiredField": "status", + "requiredAnyFields": [ + + ], + "excludedPaths": [ + "repairability.repairRecordSet[*]" + ] + } + ], + "conflictDecisions": [ + { + "id": "BILLOFMATERIALS_MATERIALSET_PORTION", + "path": "billOfMaterials.materialSet[*].portion", + "status": "RESOLVED", + "note": "D4.1 BOM UML image9; D4.1 material prose; pinned CIR4FUN.BillOfMaterials.json", + "evidence": [ + "D4.1 BOM UML image9", + "D4.1 material prose", + "pinned CIR4FUN.BillOfMaterials.json" + ] + }, + { + "id": "BILLOFMATERIALS_COMPONENTSET_MANDATORY", + "path": "billOfMaterials.componentSet[*].mandatory", + "status": "RESOLVED", + "note": "D4.1 BOM UML image9; component mandatory field optional", + "evidence": [ + "D4.1 BOM UML image9", + "component mandatory field optional" + ] + }, + { + "id": "BILLOFMATERIALS_PARTSET_MANDATORY", + "path": "billOfMaterials.partSet[*].mandatory", + "status": "RESOLVED", + "note": "D4.1 BOM UML image9; part mandatory field optional", + "evidence": [ + "D4.1 BOM UML image9", + "part mandatory field optional" + ] + }, + { + "id": "CARBONFOOTPRINT_CARBONFOOTPRINTCALCULATIONSET_QUANTITYOFMEASUREMENTFORCALCULATION", + "path": "carbonFootprint.carbonFootprintCalculationSet[*].quantityOfMeasurementForCalculation", + "status": "RESOLVED", + "note": "D4.1 CarbonFootprintCalculation UML image10", + "evidence": [ + "D4.1 CarbonFootprintCalculation UML image10" + ] + }, + { + "id": "ENVIRONMENTALIMPACT_INDICATORSET_VALUE", + "path": "environmentalImpact.indicatorSet[*].value", + "status": "RESOLVED", + "note": "D4.1 EnvironmentalImpact UML image12", + "evidence": [ + "D4.1 EnvironmentalImpact UML image12" + ] + }, + { + "id": "HEALTHIMPACT_INDICATORSET_VALUE", + "path": "healthImpact.indicatorSet[*].value", + "status": "RESOLVED", + "note": "D4.1 HealthImpact UML image13", + "evidence": [ + "D4.1 HealthImpact UML image13" + ] + }, + { + "id": "NAMEPLATE_DESCRIPTION_DESIGNATION_FAMILY_ROOT_SUFFIX_PRODUCTTYPE", + "path": "nameplate.*Description|*Designation|*Family|*Root|*Suffix|productType", + "status": "RESOLVED", + "note": "D4.1 Nameplate UML image14; D4.1 multilingual EN model mapping", + "evidence": [ + "D4.1 Nameplate UML image14", + "D4.1 multilingual EN model mapping" + ] + }, + { + "id": "NAMEPLATE_GLNOFMANUFACTURER_GLNOFSUPPLIER_HSCODEOFTHEWCO", + "path": "nameplate.glnOfManufacturer|glnOfSupplier|hsCodeOfTheWCO", + "status": "RESOLVED", + "note": "D4.1 Nameplate UML image14", + "evidence": [ + "D4.1 Nameplate UML image14" + ] + }, + { + "id": "PRODUCTRELATIONSHIP_COLLECTIONMEMBERSET_COLLECTION", + "path": "productRelationship.collectionMemberSet[*].collection", + "status": "RESOLVED", + "note": "D4.1 ProductRelationship UML image15", + "evidence": [ + "D4.1 ProductRelationship UML image15" + ] + }, + { + "id": "RECYCLABILITY_RECYCLEDCONTENT", + "path": "recyclability.recycledContent", + "status": "RESOLVED", + "note": "D4.1 Recyclability UML image16", + "evidence": [ + "D4.1 Recyclability UML image16" + ] + }, + { + "id": "REPAIRABILITY_REPAIRRECORDSET_TIME", + "path": "repairability.repairRecordSet[*].time", + "status": "RESOLVED", + "note": "D4.1 Repairability UML image20", + "evidence": [ + "D4.1 Repairability UML image20" + ] + }, + { + "id": "REPAIR_RECORD_STATUS_OMITTED", + "path": "repairability.repairRecordSet[*].status", + "status": "RESOLVED", + "note": "D4.1 Repairability UML image20 and pinned Repairability template omit status; ontology generic RecordInformation is not sufficient to add it operationally", + "evidence": [ + "D4.1 Repairability UML image20 and pinned Repairability template omit status", + "ontology generic RecordInformation is not sufficient to add it operationally" + ] + }, + { + "id": "REUSABILITY_SECONDHAND", + "path": "reusability.secondHand", + "status": "RESOLVED", + "note": "D4.1 Reusability UML image21", + "evidence": [ + "D4.1 Reusability UML image21" + ] + }, + { + "id": "SAFETY_PROFESSIONAL_USE_ONLY_TYPE", + "path": "safety.professionalUseOnly", + "status": "RESOLVED", + "note": "D4.1 prose says String, while UML Figure 17 and pinned Safety AAS declare Boolean. The operational model follows Figure 17 and the pinned AAS template; retain this source conflict explicitly.", + "evidence": [ + "D4.1 prose says String, while UML Figure 17 and pinned Safety AAS declare Boolean. The operational model follows Figure 17 and the pinned AAS template", + "retain this source conflict explicitly." + ] + }, + { + "id": "WASTEMANAGEMENT_TOXICITYRISK_FIRERISK_ELECTRICITYRISK", + "path": "wasteManagement.toxicityRisk|fireRisk|electricityRisk", + "status": "RESOLVED", + "note": "D4.1 WasteManagement UML image27", + "evidence": [ + "D4.1 WasteManagement UML image27" + ] + }, + { + "id": "CHARACTERISTICS_SURFACE", + "path": "characteristics.surface", + "status": "RESOLVED_WITH_SOURCE_CONFLICT", + "note": "D4.1 numeric total-area prose conflicts with its UML, all six pinned Characteristics AAS templates, and populated examples, which model Surface as descriptive String. Follow the published AAS String binding without adding a SurfaceArea field or semantic IRI.", + "evidence": [ + "D4.1 six Characteristics UMLs image28-image33", + "Published AAS templates declare xs:string and describe surface type; populated AAS and ontology examples use qualitative surface values." + ] + }, + { + "id": "CHARACTERISTICS_FLAMERETARDANT", + "path": "characteristics.flameRetardant", + "status": "RESOLVED", + "note": "D4.1 FabricCharacteristics UML image30; pinned template xs:string discrepancy resolved in favor of D4.1 operational profile", + "evidence": [ + "D4.1 FabricCharacteristics UML image30", + "pinned template xs:string discrepancy resolved in favor of D4.1 operational profile" + ] + }, + { + "id": "CHARACTERISTICS_COLORFASTNESS", + "path": "characteristics.colorFastness", + "status": "RESOLVED", + "note": "D4.1 FabricCharacteristics UML image30; pinned template xs:string discrepancy resolved in favor of D4.1 operational profile", + "evidence": [ + "D4.1 FabricCharacteristics UML image30", + "pinned template xs:string discrepancy resolved in favor of D4.1 operational profile" + ] + }, + { + "id": "CHARACTERISTICS_MATELASINCLUDED", + "path": "characteristics.matelasIncluded", + "status": "RESOLVED", + "note": "D4.1 CribCharacteristics UML image29; source spelling retained as catalog canonical ID and English Java alias", + "evidence": [ + "D4.1 CribCharacteristics UML image29", + "source spelling retained as catalog canonical ID and English Java alias" + ] + }, + { + "id": "TRACEABILITY_MANUFACTURINGRECORDSET", + "path": "traceability.manufacturingRecordSet[*]", + "status": "RESOLVED", + "note": "D4.1 Traceability UML image24; pinned AAS template ManufacturingRecord", + "evidence": [ + "D4.1 Traceability UML image24", + "pinned AAS template ManufacturingRecord" + ] + }, + { + "id": "CERTIFICATE_ISSUE_DATE_DATETIME", + "path": "durability.certificateSet[*].issueDate", + "status": "RESOLVED", + "note": "D4.1 prose says datetime; ontology uses xsd:date. Keep D4.1 operational datetime.", + "evidence": [ + "D4.1 section 3.2.1.3", + "Figure 5", + "pinned CIR4FUN.Durability.json", + "DPP4Fun ontology 0.2.2" + ] + }, + { + "id": "STANDARD_COMPLIANCE_STATUS_STRING", + "path": "durability.standardSet[*].complianceStatus", + "status": "RESOLVED", + "note": "D4.1 prose types compliance status as String; ontology uses Boolean. Keep D4.1 operational String.", + "evidence": [ + "D4.1 section 3.2.1.3", + "Figure 5", + "pinned CIR4FUN.Durability.json", + "DPP4Fun ontology 0.2.2" + ] + }, + { + "id": "CARBON_FOOTPRINT_IMPACT_UNIT_STRING", + "path": "carbonFootprint.carbonFootprintCalculationSet[*].impactUnitForCalculation", + "status": "RESOLVED", + "note": "D4.1 prose types impact unit as String; ontology models Unit as a semantic object. Keep operational String.", + "evidence": [ + "D4.1 section 3.2.1.2", + "Figure 4", + "pinned CIR4FUN.CarbonFootprint.json", + "DPP4Fun ontology 0.2.2" + ] + }, + { + "id": "POSITION_IN_PROCESS_ROUTING_URI", + "path": "traceability.manufacturingRecordSet[*].positionInProcessRouting", + "status": "RESOLVED", + "note": "D4.1 prose describes a URI/reference; ontology property is String. Keep URI syntax per D4.1.", + "evidence": [ + "D4.1 section 3.2.1.16", + "Figure 18", + "pinned CIR4FUN.Traceability.json", + "DPP4Fun ontology 0.2.2" + ] + }, + { + "id": "CONTACT_ORGANIZATION_ID_OMITTED", + "path": "ontology.contact.organizationID", + "status": "RESOLVED", + "note": "organizationID appears in the ontology Contact model but not in the operational AAS Contact template; omit from D4.1 typed model.", + "evidence": [ + "D4.1 Figures 8 and 33", + "pinned CIR4FUN.Nameplate.json", + "DPP4Fun ontology 0.2.2" + ] + }, + { + "id": "MAINTENANCE_RECORD_OMITTED", + "path": "ontology.maintenanceRecord", + "status": "RESOLVED", + "note": "MaintenanceRecord appears in ontology RecordInformation but is absent from operational D4.1 Traceability and Repairability templates; omit.", + "evidence": [ + "D4.1 Figures 14 and 18", + "pinned CIR4FUN.Repairability.json", + "pinned CIR4FUN.Traceability.json", + "DPP4Fun ontology 0.2.2" + ] + }, + { + "id": "CHARACTERISTICS_SURFACE_DICTIONARY", + "path": "characteristics.surface", + "status": "RESOLVED_WITH_SOURCE_CONFLICT", + "note": "Use xsd:string for the published surface-type binding; D4.1 numeric total-area prose conflicts with its UML and AAS examples. Do not add a SurfaceArea field or semantic IRI.", + "evidence": [ + "D4.1 Figures 22-27 and field prose", + "pinned Characteristics AAS templates", + "DPP4Fun ontology 0.2.2" + ] + }, + { + "id": "FIGURE_25_LOCKER_CAPTION", + "path": "characteristics.locker", + "status": "RESOLVED", + "note": "Figure 25 caption says Fabric, while its section and UML class box identify Locker; use Locker.", + "evidence": [ + "D4.1 section 3.2.2.4", + "Figure 25", + "pinned CIR4FUN.LockerCharacteristics.json" + ] + }, + { + "id": "MATELAS_INCLUDED_JAVA_ALIAS", + "path": "characteristics.matelasIncluded", + "status": "RESOLVED", + "note": "Preserve source spelling MatelasIncluded and expose mattressIncluded as the Java-facing alias.", + "evidence": [ + "D4.1 Figure 23", + "pinned CIR4FUN.CribCharacteristics.json", + "DPP4Fun ontology 0.2.2" + ] + }, + { + "id": "BOM_MANDATORY_CARDINALITY", + "path": "billOfMaterials.*.mandatory", + "status": "RESOLVED", + "note": "Material Mandatory is explicitly described as Boolean; Component and Part Mandatory appear optional in Figure 3/UML. Keep Boolean fields optional and scalar source cardinality UNSPECIFIED.", + "evidence": [ + "D4.1 section 3.2.1.1", + "Figure 3", + "pinned CIR4FUN.BillOfMaterials.json", + "DPP4Fun ontology 0.2.2" + ] + } + ] +} diff --git a/src/dpp_sdk/dpp4fun/transport.py b/src/dpp_sdk/dpp4fun/transport.py index 43ea4a7..03be599 100644 --- a/src/dpp_sdk/dpp4fun/transport.py +++ b/src/dpp_sdk/dpp4fun/transport.py @@ -1,111 +1,78 @@ -"""JSON transport for the furniture DPP (port of ``Dpp4FunJsonCodec``). - -The canonical model nests the reusable fields under ``coreDpp``. For transport, -the outbound JSON is *flattened* — ``passportMetadata``, ``nameplate`` and -``documentation`` are lifted to the root — while the inbound parser accepts -*either* the flat or the nested shape and normalizes to nested before validation. - -This module also serves as a ``DppCodec`` for :class:`Dpp4Fun` when used with the -repository client. -""" +"""DPP4Fun facade over canonical core JSON transport.""" from __future__ import annotations -import json -from typing import Any - -from pydantic import ValidationError - -from ..core.errors import DppMappingError -from .model import Dpp4Fun -from .validation import validate_dpp4fun - -_CORE_FIELDS = ("passportMetadata", "nameplate", "documentation") +from ..core.errors import DppValidationError +from ..core.json_codec import CompressedExpansionResult, DppJsonCodec +from ..core.model import DigitalProductPassport +from ..core.validation import IdentifierValidationContext +from .profile import Dpp4FunSemanticResolver +from .typed_model import Dpp4Fun -def _reject_non_finite_constant(token: str) -> Any: - raise ValueError(f"non-finite JSON number is not allowed: {token}") - - -def _move_if_present(source: dict[str, Any], target: dict[str, Any], field: str) -> None: - """Move ``field`` from ``source`` to ``target`` if present (incl. a null value).""" - if field in source: - target[field] = source.pop(field) - +class Dpp4FunJsonCodec: + """Typed DPP4Fun convenience operations backed by the core codec.""" -def _flatten_core(root: dict[str, Any]) -> None: - """Lift the core submodels out of ``coreDpp`` up to the root (outbound).""" - core = root.pop("coreDpp", None) - if not isinstance(core, dict): - return - for field in _CORE_FIELDS: - _move_if_present(core, root, field) + def __init__(self) -> None: + self._codec = DppJsonCodec() + self._resolver = Dpp4FunSemanticResolver() + def to_json(self, value: Dpp4Fun | None) -> str: + return self.to_expanded_json(value) -def _normalize_transport(root: dict[str, Any]) -> None: - """Normalize a flat-or-nested inbound shape to the canonical nested shape.""" - has_core = isinstance(root.get("coreDpp"), dict) + def to_expanded_json(self, value: Dpp4Fun | None) -> str: + return self._codec.to_expanded_json(None if value is None else value.passport) # type: ignore[arg-type] - if not has_core: - created_core: dict[str, Any] = {} - for field in _CORE_FIELDS: - _move_if_present(root, created_core, field) - if created_core: - root["coreDpp"] = created_core - return + def to_compressed_json(self, value: Dpp4Fun | None) -> str: + return self._codec.to_compressed_json(None if value is None else value.passport) # type: ignore[arg-type] - # coreDpp already present: drop any duplicate flat keys at the root. - for field in _CORE_FIELDS: - root.pop(field, None) - if not root["coreDpp"]: - del root["coreDpp"] + def from_json(self, json_text: str) -> Dpp4Fun: + return self.from_expanded_json(json_text) + def from_expanded_json(self, json_text: str) -> Dpp4Fun: + return Dpp4Fun.from_passport(self._codec.from_expanded_json(json_text)) -def to_json(dpp: Dpp4Fun) -> str: - """Serialize a :class:`Dpp4Fun` to its flat transport JSON string.""" - try: - data = dpp.model_dump(mode="json") - _flatten_core(data) - return json.dumps(data, allow_nan=False) - except ValueError as exc: - raise DppMappingError(f"Failed to serialize DPP to JSON: {exc}") from exc + def expand_compressed_json( + self, + json_text: str, + previous: DigitalProductPassport | Dpp4Fun | None = None, + ) -> CompressedExpansionResult: + prior_passport = previous.passport if isinstance(previous, Dpp4Fun) else previous + return self._codec.expand_compressed_json(json_text, prior_passport, self._resolver) + def from_compressed_json(self, json_text: str) -> Dpp4Fun: + return Dpp4Fun.from_passport(self.expand_compressed_json(json_text).passport) -def from_json(raw: str) -> Dpp4Fun: - """Parse a :class:`Dpp4Fun` from JSON, accepting the flat or nested shape. + def from_json_and_validate( + self, json_text: str, context: IdentifierValidationContext | None = None + ) -> Dpp4Fun: + value = self.from_expanded_json(json_text) + self._require_valid(value, context) + return value - Does not apply semantic validation (mirrors the lenient ``fromJson``). - """ - try: - tree = json.loads(raw, parse_constant=_reject_non_finite_constant) - except (json.JSONDecodeError, ValueError) as exc: - raise DppMappingError(f"Failed to deserialize DPP JSON: {exc}") from exc - if isinstance(tree, dict): - _normalize_transport(tree) - try: - return Dpp4Fun.model_validate(tree) - except ValidationError as exc: - raise DppMappingError(f"Failed to map DPP JSON to Dpp4Fun: {exc}") from exc + def from_compressed_json_and_validate(self, json_text: str) -> Dpp4Fun: + value = self.from_compressed_json(json_text) + self._require_valid(value) + return value + @staticmethod + def _require_valid(value: Dpp4Fun, context: IdentifierValidationContext | None = None) -> None: + from .validation import Dpp4FunValidationService -def from_json_and_validate(raw: str) -> Dpp4Fun: - """Parse and then fully validate a :class:`Dpp4Fun` (fail-fast).""" - dpp = from_json(raw) - validate_dpp4fun(dpp) - return dpp + result = Dpp4FunValidationService().validate(value, context) + if not result.is_valid: + raise DppValidationError(f"Dpp4Fun validation failed: {result.issues}") -class Dpp4FunJsonCodec: - """Object-oriented facade mirroring the Java ``Dpp4FunJsonCodec``. +def to_json(value: Dpp4Fun | None) -> str: + return Dpp4FunJsonCodec().to_json(value) - Also usable as a ``DppCodec[Dpp4Fun]`` with the repository client. - """ - def to_json(self, dpp: Dpp4Fun) -> str: - return to_json(dpp) +def from_json(json_text: str) -> Dpp4Fun: + return Dpp4FunJsonCodec().from_json(json_text) - def from_json(self, raw: str) -> Dpp4Fun: - return from_json(raw) - def from_json_and_validate(self, raw: str) -> Dpp4Fun: - return from_json_and_validate(raw) +def from_json_and_validate( + json_text: str, context: IdentifierValidationContext | None = None +) -> Dpp4Fun: + return Dpp4FunJsonCodec().from_json_and_validate(json_text, context) diff --git a/src/dpp_sdk/dpp4fun/typed_model.py b/src/dpp_sdk/dpp4fun/typed_model.py new file mode 100644 index 0000000..38b789f --- /dev/null +++ b/src/dpp_sdk/dpp4fun/typed_model.py @@ -0,0 +1,511 @@ +"""Typed immutable DPP4Fun views over one canonical core passport.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from datetime import UTC, datetime +from typing import Any, ClassVar, Self, TypeAlias, cast + +from pydantic import BaseModel, ConfigDict + +from ..core.model import ( + DataElement, + DataElementCollection, + DigitalProductPassport, + DppHeader, + MultiLanguageDataElement, + MultiLanguageValue, + MultiValuedDataElement, + RelatedResource, + SingleValuedDataElement, +) +from .profile import DPP4FUN_PROFILE, Dpp4FunElementDefinition, profile_definition +from .units import CustomSemanticUnit, Dpp4FunQuantity, UnitDimension, _known_unit + +CoreElement: TypeAlias = ( + DataElementCollection + | SingleValuedDataElement + | MultiValuedDataElement + | MultiLanguageDataElement + | RelatedResource +) + + +def _children(element: DataElementCollection) -> tuple[CoreElement, ...]: + return element.elements + + +def _ordered_children( + path: str, children: Sequence[CoreElement], *, variant: str | None = None +) -> tuple[CoreElement, ...]: + definitions = [ + definition + for definition in DPP4FUN_PROFILE.definitions + if definition.path.rpartition(".")[0] == path + and (variant is None or f"characteristics-{variant}" in definition.applies_to) + ] + order = {definition.element_id: index for index, definition in enumerate(definitions)} + return tuple( + child + for _, child in sorted( + enumerate(children), + key=lambda pair: (order.get(pair[1].element_id or "", len(order)), pair[0]), + ) + ) + + +def _element_for(definition: Dpp4FunElementDefinition, value: object) -> CoreElement: + if isinstance(value, DataElement): + if value.object_type != definition.object_type: + raise TypeError( + f"{definition.path} requires {definition.object_type}, got {value.object_type}" + ) + element = cast(CoreElement, value) + return element + if definition.object_type == "SingleValuedDataElement": + datatype = definition.value_data_type + if datatype == "xsd:string" and not isinstance(value, str): + raise TypeError(f"{definition.path} requires a string") + if datatype == "xsd:boolean" and not isinstance(value, bool): + raise TypeError(f"{definition.path} requires a boolean") + if datatype == "xsd:integer" and (not isinstance(value, int) or isinstance(value, bool)): + raise TypeError(f"{definition.path} requires an integer") + if datatype == "xsd:double": + if not isinstance(value, (int, float)) or isinstance(value, bool): + raise TypeError(f"{definition.path} requires a number") + value = float(value) + if datatype in {"xsd:anyURI", "xsd:dateTime"} and not isinstance(value, str): + raise TypeError(f"{definition.path} requires a string representation") + return SingleValuedDataElement( + element_id=definition.element_id, + object_type="SingleValuedDataElement", + dictionary_reference=definition.dictionary_reference, + value_data_type=definition.value_data_type, + value=value, + ) + if definition.object_type == "MultiValuedDataElement": + if not isinstance(value, Sequence) or isinstance(value, (str, bytes)): + raise TypeError(f"{definition.path} requires an ordered sequence of DataElements") + item_definitions = DPP4FUN_PROFILE.by_path.get(f"{definition.path}[*]", ()) + item_definition = item_definitions[0] if item_definitions else None + if item_definition and item_definition.object_type == "SingleValuedDataElement": + value = tuple(_element_for(item_definition, item) for item in value) + if item_definitions and item_definitions[0].repeated_item_policy: + pattern = item_definitions[0].repeated_item_policy.get("elementIdPattern") + if pattern: + normalized: list[CoreElement] = [] + for index, item in enumerate(value): + if not isinstance(item, DataElement): + raise TypeError(f"{definition.path} items must be DataElements") + normalized.append( + cast(CoreElement, item.with_updates(element_id=pattern % index)) + ) + value = normalized + return MultiValuedDataElement( + element_id=definition.element_id, + object_type="MultiValuedDataElement", + dictionary_reference=definition.dictionary_reference, + value=tuple(value), + ) + if definition.object_type == "MultiLanguageDataElement": + if not isinstance(value, Mapping) or any( + not isinstance(language, str) or not isinstance(text, str) + for language, text in value.items() + ): + raise TypeError(f"{definition.path} requires a language-to-text mapping") + return MultiLanguageDataElement( + element_id=definition.element_id, + object_type="MultiLanguageDataElement", + dictionary_reference=definition.dictionary_reference, + value=tuple( + MultiLanguageValue(value=text, language=language) + for language, text in value.items() + ), + ) + if definition.object_type == "DataElementCollection": + if isinstance(value, Mapping): + entries = tuple(value.values()) + elif isinstance(value, Sequence) and not isinstance(value, (str, bytes)): + entries = tuple(value) + else: + raise TypeError( + f"{definition.path} requires DataElements or a mapping of child elements" + ) + if any(not isinstance(entry, DataElement) for entry in entries): + raise TypeError(f"{definition.path} children must be DataElements") + return DataElementCollection( + element_id=definition.element_id, + object_type="DataElementCollection", + dictionary_reference=definition.dictionary_reference, + elements=entries, + ) + raise TypeError(f"Unsupported profile DataElement kind {definition.object_type!r}") + + +class ProfiledAspect(BaseModel): + """Immutable typed view over a profile collection, with unknown children retained.""" + + model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True, extra="forbid") + + element: DataElementCollection + element_id: ClassVar[str] = "characteristics" + variant: ClassVar[Any | None] = None + field_aliases: ClassVar[dict[str, str]] = {} + + @classmethod + def create(cls, **fields: object) -> Self: + root_definition = profile_definition( + cls.element_id, variant=getattr(cls.variant, "value", None) + ) + children = tuple( + _element_for( + profile_definition( + f"{cls.element_id}.{cls.field_aliases.get(name, name)}", + variant=getattr(cls.variant, "value", None), + ), + value, + ) + for name, value in fields.items() + if value is not None + ) + children = _ordered_children( + cls.element_id, children, variant=getattr(cls.variant, "value", None) + ) + element = DataElementCollection( + element_id=root_definition.element_id, + object_type="DataElementCollection", + dictionary_reference=root_definition.dictionary_reference, + elements=children, + ) + return cls(element=element) + + @classmethod + def from_element(cls, element: DataElementCollection) -> Self: + if element.element_id != cls.element_id: + raise ValueError(f"expected aspect {cls.element_id!r}, got {element.element_id!r}") + return cls(element=element) + + def with_field(self, name: str, value: object | None) -> Self: + path = f"{self.element_id}.{self.field_aliases.get(name, name)}" + definition = profile_definition(path, variant=getattr(self.variant, "value", None)) + retained = tuple( + child for child in _children(self.element) if child.element_id != definition.element_id + ) + children = retained if value is None else (*retained, _element_for(definition, value)) + children = _ordered_children( + self.element_id, children, variant=getattr(self.variant, "value", None) + ) + return type(self)(element=self.element.with_updates(elements=children)) + + def field(self, name: str) -> CoreElement | None: + definition = profile_definition( + f"{self.element_id}.{self.field_aliases.get(name, name)}", + variant=getattr(self.variant, "value", None), + ) + return next( + (child for child in self.element.elements if child.element_id == definition.element_id), + None, + ) + + def as_data_element(self) -> DataElementCollection: + return self.element + + def with_quantity(self, name: str, quantity: Dpp4FunQuantity) -> Self: + definition = profile_definition( + f"{self.element_id}.{name}", variant=getattr(self.variant, "value", None) + ) + if definition.unit_dimension is None or definition.unit_slot_path is None: + raise ValueError(f"{name!r} is not a quantity field in the DPP4Fun profile") + expected_dimension = UnitDimension(definition.unit_dimension.upper()) + if quantity.unit.dimension != expected_dimension: + raise ValueError( + f"DPP4FUN_UNIT_DIMENSION_MISMATCH: {name} requires {expected_dimension.value}" + ) + if isinstance(quantity.unit, CustomSemanticUnit): + raise ValueError("custom semantic units cannot be preserved in DPP4Fun unit slots") + unit_slot = definition.unit_slot_path + current_unit = next( + (child for child in self.element.elements if child.element_id == unit_slot), None + ) + if ( + current_unit is not None + and isinstance(current_unit, SingleValuedDataElement) + and current_unit.value != quantity.unit.symbol + ): + raise ValueError(f"DPP4FUN_UNIT_SLOT_CONFLICT: {unit_slot} already has another unit") + return self.with_field(name, quantity.value).with_field(unit_slot, quantity.unit.symbol) + + def quantity(self, name: str) -> Dpp4FunQuantity | None: + definition = profile_definition( + f"{self.element_id}.{name}", variant=getattr(self.variant, "value", None) + ) + if definition.unit_dimension is None or definition.unit_slot_path is None: + return None + by_id = {child.element_id: child for child in self.element.elements} + value = by_id.get(definition.element_id) + unit = by_id.get(definition.unit_slot_path) + if ( + not isinstance(value, SingleValuedDataElement) + or not isinstance(value.value, (int, float)) + or isinstance(value.value, bool) + ): + return None + if not isinstance(unit, SingleValuedDataElement) or not isinstance(unit.value, str): + return None + resolved = _known_unit(unit.value, UnitDimension(definition.unit_dimension.upper())) + if resolved is None: + return None + return Dpp4FunQuantity(value=float(value.value), unit=resolved) + + +class Nameplate(ProfiledAspect): + element_id = "nameplate" + + +class HealthImpact(ProfiledAspect): + element_id = "healthImpact" + + +class EnvironmentalImpact(ProfiledAspect): + element_id = "environmentalImpact" + + +class Durability(ProfiledAspect): + element_id = "durability" + + +class CarbonFootprint(ProfiledAspect): + element_id = "carbonFootprint" + + +class BillOfMaterials(ProfiledAspect): + element_id = "billOfMaterials" + + +class Refurbishment(ProfiledAspect): + element_id = "refurbishment" + + +class Recyclability(ProfiledAspect): + element_id = "recyclability" + + +class ProductRelationship(ProfiledAspect): + element_id = "productRelationship" + + +class Remanufacturing(ProfiledAspect): + element_id = "remanufacturing" + + +class Reliability(ProfiledAspect): + element_id = "reliability" + + +class Repairability(ProfiledAspect): + element_id = "repairability" + + +class Reusability(ProfiledAspect): + element_id = "reusability" + + +class WasteManagement(ProfiledAspect): + element_id = "wasteManagement" + + +class Usage(ProfiledAspect): + element_id = "usage" + + +class Upgradability(ProfiledAspect): + element_id = "upgradability" + + +class Traceability(ProfiledAspect): + element_id = "traceability" + + +class Safety(ProfiledAspect): + element_id = "safety" + + +class RightOfEndUsers(ProfiledAspect): + element_id = "rightOfEndUsers" + + +class Dpp4Fun(BaseModel): + """Typed facade whose sole passport state is one immutable core aggregate.""" + + model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True, extra="forbid") + + passport: DigitalProductPassport + SCHEMA_VERSION: ClassVar[str] = "1.0" + + @classmethod + def from_passport(cls, passport: DigitalProductPassport) -> Self: + seen: set[str] = set() + for element in passport.data_elements: + if element.element_id in DPP4FUN_PROFILE.root_element_ids: + if element.element_id in seen: + raise ValueError(f"duplicate reserved DPP4Fun aspect {element.element_id!r}") + if not isinstance(element, DataElementCollection): + raise ValueError( + f"reserved DPP4Fun aspect {element.element_id!r} must be a collection" + ) + seen.add(element.element_id or "") + selected_variants = [ + variant_id + for variant_id in DPP4FUN_PROFILE.variants.values() + if variant_id in passport.dpp_header.content_specification_ids + ] + if len(selected_variants) > 1: + raise ValueError("a Dpp4Fun passport can select only one Characteristics variant") + characteristics = next( + ( + element + for element in passport.data_elements + if element.element_id == "characteristics" + ), + None, + ) + if characteristics is not None and not isinstance(characteristics, DataElementCollection): + raise ValueError("reserved Dpp4Fun Characteristics must be a collection") + return cls(passport=passport) + + @classmethod + def create( + cls, + header: DppHeader, + *aspects: ProfiledAspect, + extensions: Sequence[CoreElement] = (), + ) -> Self: + selected = [aspect.as_data_element() for aspect in aspects] + ids = [element.element_id for element in selected] + if len(ids) != len(set(ids)): + raise ValueError("Dpp4Fun aspects must have unique element IDs") + if any(element.element_id in DPP4FUN_PROFILE.root_element_ids for element in extensions): + raise ValueError("extensions cannot replace a reserved Dpp4Fun aspect") + variant_ids = { + DPP4FUN_PROFILE.variants[aspect.variant.value] + for aspect in aspects + if aspect.variant is not None + } + reserved_ids = set(DPP4FUN_PROFILE.variants.values()) | {"DPP4FUN_COMMON_V1"} + specs = tuple(spec for spec in header.content_specification_ids if spec not in reserved_ids) + has_catalog_aspect = any( + element.element_id in DPP4FUN_PROFILE.root_element_ids for element in selected + ) + if has_catalog_aspect: + specs = tuple(dict.fromkeys((*specs, "DPP4FUN_COMMON_V1", *variant_ids))) + header_data = header.model_dump(mode="json", by_alias=True) + header_data["contentSpecificationIds"] = specs + header_data["dppSchemaVersion"] = cls.SCHEMA_VERSION + header_data["lastUpdated"] = header.last_updated or datetime.now(UTC) + header = DppHeader.model_validate(header_data) + order = { + element_id: index for index, element_id in enumerate(DPP4FUN_PROFILE.root_element_ids) + } + elements = tuple( + sorted(selected, key=lambda element: order.get(element.element_id or "", len(order))) + ) + tuple(extensions) + return cls.from_passport(DigitalProductPassport(dpp_header=header, data_elements=elements)) + + def as_digital_product_passport(self) -> DigitalProductPassport: + return self.passport + + @property + def header(self) -> DppHeader: + return self.passport.dpp_header + + def get_aspect(self, element_id: str) -> DataElementCollection | None: + value = self.passport.find_data_element((element_id,)) + if value is None: + return None + if not isinstance(value, DataElementCollection): + raise ValueError(f"reserved Dpp4Fun aspect {element_id!r} must be a collection") + return value + + def typed_aspect(self, aspect_type: type[ProfiledAspect]) -> ProfiledAspect | None: + element = self.get_aspect(aspect_type.element_id) + return None if element is None else aspect_type.from_element(element) + + def selected_characteristics(self) -> ProfiledAspect | None: + from .characteristics import ( + ChairCharacteristics, + CribCharacteristics, + Dpp4FunProductVariant, + FabricCharacteristics, + LockerCharacteristics, + ParticleBoardCharacteristics, + SofaCharacteristics, + ) + + selected = { + DPP4FUN_PROFILE.variants[variant.value]: variant + for variant in Dpp4FunProductVariant + if DPP4FUN_PROFILE.variants[variant.value] in self.header.content_specification_ids + } + if not selected: + return None + variant = next(iter(selected.values())) + types = { + Dpp4FunProductVariant.CHAIR: ChairCharacteristics, + Dpp4FunProductVariant.CRIB: CribCharacteristics, + Dpp4FunProductVariant.FABRIC: FabricCharacteristics, + Dpp4FunProductVariant.LOCKER: LockerCharacteristics, + Dpp4FunProductVariant.PARTICLE_BOARD: ParticleBoardCharacteristics, + Dpp4FunProductVariant.SOFA: SofaCharacteristics, + } + return self.typed_aspect(types[variant]) + + def with_updates( + self, + *, + header: DppHeader | None = None, + aspects: Sequence[ProfiledAspect] = (), + ) -> Self: + aspect_ids = [aspect.element_id for aspect in aspects] + if len(aspect_ids) != len(set(aspect_ids)): + raise ValueError("Dpp4Fun aspect updates must have unique element IDs") + replacements = {aspect.element_id: aspect.as_data_element() for aspect in aspects} + current = list(self.passport.data_elements) + for element_id, replacement in replacements.items(): + for index, old in enumerate(current): + if old.element_id == element_id: + current[index] = replacement + break + else: + current.append(replacement) + order = { + element_id: index for index, element_id in enumerate(DPP4FUN_PROFILE.root_element_ids) + } + current.sort(key=lambda element: order.get(element.element_id or "", len(order))) + updated_header = header or self.passport.dpp_header + if any(aspect.variant is not None for aspect in aspects): + known_variant_ids = set(DPP4FUN_PROFILE.variants.values()) + selected_ids = tuple( + DPP4FUN_PROFILE.variants[aspect.variant.value] + for aspect in aspects + if aspect.variant is not None + ) + specs = ( + tuple( + spec + for spec in updated_header.content_specification_ids + if spec not in known_variant_ids + ) + + selected_ids + ) + updated_header = updated_header.with_updates( + content_specification_ids=tuple(dict.fromkeys(specs)) + ) + header_data = updated_header.model_dump(mode="json", by_alias=True) + header_data["dppSchemaVersion"] = self.SCHEMA_VERSION + header_data["lastUpdated"] = updated_header.last_updated or datetime.now(UTC) + updated_header = DppHeader.model_validate(header_data) + passport = self.passport.with_updates( + dpp_header=updated_header, + elements=tuple(current), + ) + return type(self).from_passport(passport) diff --git a/src/dpp_sdk/dpp4fun/units.py b/src/dpp_sdk/dpp4fun/units.py new file mode 100644 index 0000000..ef463a9 --- /dev/null +++ b/src/dpp_sdk/dpp4fun/units.py @@ -0,0 +1,108 @@ +"""Semantic units and finite quantities used by DPP4Fun Characteristics.""" + +from __future__ import annotations + +from enum import StrEnum +from typing import Annotated, Protocol +from urllib.parse import urlparse + +from pydantic import AfterValidator, BaseModel, ConfigDict, Field, model_validator + + +class UnitDimension(StrEnum): + LENGTH = "LENGTH" + MASS = "MASS" + VOLUME = "VOLUME" + + +class SemanticUnit(Protocol): + @property + def symbol(self) -> str: ... + + @property + def semantic_id(self) -> str: ... + + @property + def dimension(self) -> UnitDimension: ... + + +class _Unit(StrEnum): + @property + def symbol(self) -> str: + return self.value.split("|", 1)[0] + + @property + def semantic_id(self) -> str: + return self.value.split("|", 1)[1] + + @property + def dimension(self) -> UnitDimension: + return { + "LengthUnit": UnitDimension.LENGTH, + "MassUnit": UnitDimension.MASS, + "VolumeUnit": UnitDimension.VOLUME, + }[type(self).__name__] + + +class LengthUnit(_Unit): + MILLIMETER = "mm|http://qudt.org/vocab/unit/MilliM" + CENTIMETER = "cm|http://qudt.org/vocab/unit/CentiM" + METER = "m|http://qudt.org/vocab/unit/M" + INCH = "in|http://qudt.org/vocab/unit/IN" + + +class MassUnit(_Unit): + KILOGRAM = "kg|http://qudt.org/vocab/unit/KiloGM" + GRAM = "g|http://qudt.org/vocab/unit/GM" + POUND = "lb|http://qudt.org/vocab/unit/LB_M" + + +class VolumeUnit(_Unit): + CUBIC_METER = "m³|http://qudt.org/vocab/unit/M3" + LITER = "L|http://qudt.org/vocab/unit/L" + + +KnownSemanticUnit = LengthUnit | MassUnit | VolumeUnit + + +def _known_unit(symbol: str, dimension: UnitDimension) -> KnownSemanticUnit | None: + units: tuple[KnownSemanticUnit, ...] = (*LengthUnit, *MassUnit, *VolumeUnit) + return next( + (unit for unit in units if unit.symbol == symbol and unit.dimension == dimension), + None, + ) + + +def _absolute_uri(value: str) -> str: + parsed = urlparse(value) + if not parsed.scheme: + raise ValueError("semantic_id must be an absolute URI") + return value + + +class CustomSemanticUnit(BaseModel): + model_config = ConfigDict(frozen=True, extra="forbid") + + symbol: Annotated[str, Field(min_length=1)] + semantic_id: Annotated[str, AfterValidator(_absolute_uri)] + dimension: UnitDimension + + @model_validator(mode="after") + def non_blank_symbol(self) -> CustomSemanticUnit: + if not self.symbol.strip(): + raise ValueError("symbol is required") + return self + + +class Dpp4FunQuantity(BaseModel): + model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True, extra="forbid") + + value: float = Field(strict=True, allow_inf_nan=False) + unit: LengthUnit | MassUnit | VolumeUnit | CustomSemanticUnit + dimension: UnitDimension | None = None + + @model_validator(mode="after") + def unit_dimension_matches(self) -> Dpp4FunQuantity: + if self.dimension is not None and self.unit.dimension != self.dimension: + raise ValueError("unit dimension does not match quantity dimension") + return self diff --git a/src/dpp_sdk/dpp4fun/validation.py b/src/dpp_sdk/dpp4fun/validation.py index cba43c5..da61265 100644 --- a/src/dpp_sdk/dpp4fun/validation.py +++ b/src/dpp_sdk/dpp4fun/validation.py @@ -1,213 +1,519 @@ -"""Semantic validators for the furniture DPP (port of ``dppsdk.dpp4fun.validation``). - -Fail-fast: raises :class:`~dpp_sdk.core.errors.DppValidationError` on the first -violation. :func:`validate_dpp4fun` is the top-level entry point; it delegates to -the core validators, the furniture sub-validators, and the two cross-object rules. -""" +"""Core plus source-backed D4.1 validation for typed DPP4Fun passports.""" from __future__ import annotations -from collections.abc import Sequence -from math import isfinite - -from ..core._text import is_blank, normalize_for_comparison, strip_contract_whitespace -from ..core.errors import DppValidationError -from ..core.validation import validate_dpp_core -from .model import ( - BillOfMaterials, - Characteristics, - Component, - Dimensions, - Dpp4Fun, - Material, - Part, - ProductClassification, +import math +import re +from collections import Counter +from collections.abc import Mapping, Sequence +from decimal import Decimal +from urllib.parse import urlsplit + +from ..core.model import ( + DataElement, + DataElementCollection, + MultiLanguageDataElement, + MultiValuedDataElement, + RelatedResource, + SingleValuedDataElement, ) +from ..core.semantic import DataElementKind, SemanticPath +from ..core.validation import ( + DataFormatValidator, + DppValidator, + IdentifierValidationContext, + IdentifierValidator, + ValidationIssue, + ValidationResult, + ValidationSeverity, +) +from .profile import DPP4FUN_PROFILE, Dpp4FunElementDefinition, Dpp4FunSemanticResolver +from .typed_model import Dpp4Fun +from .units import UnitDimension, _known_unit + +_KINDS = { + "DataElementCollection": DataElementKind.COLLECTION, + "SingleValuedDataElement": DataElementKind.SINGLE, + "MultiValuedDataElement": DataElementKind.MULTI_VALUED, + "MultiLanguageDataElement": DataElementKind.MULTI_LANGUAGE, + "RelatedResource": DataElementKind.RELATED_RESOURCE, +} + + +def _issue( + path: str, rule: str, message: str, severity: ValidationSeverity = ValidationSeverity.ERROR +) -> ValidationIssue: + return ValidationIssue(path, rule, message, severity) + + +def _matches(pattern: str, path: str) -> bool: + expected, actual = pattern.split("."), path.split(".") + expected_index = actual_index = 0 + while expected_index < len(expected) and actual_index < len(actual): + segment = expected[expected_index] + if segment.endswith("[*]"): + if ( + segment[:-3] != actual[actual_index] + or actual_index + 1 >= len(actual) + or not actual[actual_index + 1].isdecimal() + ): + return False + actual_index += 2 + elif segment != actual[actual_index]: + return False + else: + actual_index += 1 + expected_index += 1 + return expected_index == len(expected) and actual_index == len(actual) + + +def _rule_path_matches(pattern: str, path: str) -> bool: + expression = re.escape(pattern).replace(r"\[\*\]", r"\[\d+\]") + return re.fullmatch(expression, path) is not None + + +def _definition(path: str, content_ids: Sequence[str]) -> Dpp4FunElementDefinition | None: + ids = set(content_ids) + applicable = {"general"} if DPP4FUN_PROFILE.metadata["contentSpecificationId"] in ids else set() + applicable.update( + f"characteristics-{variant}" + for variant, identifier in DPP4FUN_PROFILE.variants.items() + if identifier in ids + ) + return next( + ( + item + for item in DPP4FUN_PROFILE.definitions + if applicable.intersection(item.applies_to) and _matches(item.path, path) + ), + None, + ) -def _has_text(value: str | None) -> bool: - return value is not None and not is_blank(value) - - -def _require_not_blank(value: str | None, field_name: str) -> None: - if is_blank(value): - raise DppValidationError(f"{field_name} must not be blank") - - -def _require_clean_string_list(items: Sequence[str], list_name: str) -> None: - """No null, blank, or (case-insensitive, trimmed) duplicate entries.""" - seen: set[str] = set() - for index, item in enumerate(items): - if item is None: - raise DppValidationError(f"{list_name}[{index}] must not be null") - if is_blank(item): - raise DppValidationError(f"{list_name}[{index}] must not be blank") - key = normalize_for_comparison(item) - if key in seen: - raise DppValidationError( - f"{list_name} contains duplicate entry: '{strip_contract_whitespace(item)}'" - ) - seen.add(key) +def _defined_for_any_variant(path: str) -> bool: + return any( + any(f"characteristics-{variant}" in item.applies_to for variant in DPP4FUN_PROFILE.variants) + and _matches(item.path, path) + for item in DPP4FUN_PROFILE.definitions + ) -def validate_dimensions(dimensions: Dimensions | None) -> None: - if dimensions is None: - return - for value, name in ( - (dimensions.width, "Dimensions.width"), - (dimensions.height, "Dimensions.height"), - (dimensions.depth, "Dimensions.depth"), - ): - if value is not None and (not isfinite(value) or value < 0): - raise DppValidationError(f"{name} must be finite and non-negative, but got {value}") - - has_any_value = ( - dimensions.width is not None - or dimensions.height is not None - or dimensions.depth is not None - ) - if not has_any_value: - raise DppValidationError( - "Dimensions object exists but no dimension values (width/height/depth) are provided" +def _value_matches(datatype: str | None, value: object) -> bool: + if datatype in {"xsd:string", "xsd:anyURI", "xsd:dateTime"}: + return isinstance(value, str) + if datatype == "xsd:boolean": + return isinstance(value, bool) + if datatype == "xsd:integer": + return isinstance(value, int) and not isinstance(value, bool) + if datatype in {"xsd:double", "xsd:decimal"}: + return ( + isinstance(value, (int, float, Decimal)) + and not isinstance(value, bool) + and math.isfinite(value) ) - _require_not_blank(dimensions.unit, "Dimensions.unit") + return False + + +def _uri_syntax_valid(value: str) -> bool: + if not value or any(ord(char) <= 0x20 or ord(char) >= 0x7F for char in value): + return False + if re.search(r"%(?![0-9A-Fa-f]{2})", value): + return False + try: + parsed = urlsplit(value) + _ = parsed.port + return True + except ValueError: + return False + + +def _wrong_unit_dimension(value: object, dimension: str) -> bool: + if not isinstance(value, str): + return False + expected = dimension.removesuffix("-unit").upper() + return any( + name != expected and _known_unit(value, UnitDimension(name)) is not None + for name in ("LENGTH", "MASS", "AREA", "VOLUME") + if name in UnitDimension.__members__ + ) -def validate_material(material: Material | None) -> None: - if material is None: - return - _require_not_blank(material.name, "Material.name") - if not isfinite(material.portion) or material.portion < 0: - raise DppValidationError( - f"Material.portion must be finite and non-negative, but got {material.portion}" +def _minimum_issues(elements: Sequence[DataElement], prefix: str = "") -> list[ValidationIssue]: + issues: list[ValidationIssue] = [] + rules = DPP4FUN_PROFILE.metadata.get("semanticRules", ()) + for element in elements: + element_id = element.element_id or "" + path = f"{prefix}.{element_id}" if prefix else element_id + if isinstance(element, (DataElementCollection, MultiValuedDataElement)): + children = ( + element.elements if isinstance(element, DataElementCollection) else element.value + ) + if isinstance(element, MultiValuedDataElement): + parent_rules = [ + rule + for rule in rules + if path.lower().endswith(str(rule["pathSuffix"]).replace("[*]", "").lower()) + ] + for index, item in enumerate(children): + item_path = f"{path}[{index}]" + if isinstance(item, DataElementCollection): + for rule in parent_rules: + if any( + _rule_path_matches(str(excluded), item_path) + for excluded in rule["excludedPaths"] + ): + continue + required = rule["requiredField"] + child_ids = {child.element_id for child in item.elements} + if required and required not in child_ids: + issues.append( + _issue( + f"{item_path}.{required}", + str(rule["id"]), + f"field {required} is required by D4.1", + ) + ) + elif ( + not required + and rule["requiredAnyFields"] + and not child_ids.intersection(rule["requiredAnyFields"]) + ): + field = str(rule["requiredAnyFields"][0]) + issues.append( + _issue( + f"{item_path}.{field}", + str(rule["id"]), + "a source-backed time field is required by D4.1", + ) + ) + if isinstance(item, DataElementCollection): + issues.extend(_minimum_issues(item.elements, item_path)) + else: + issues.extend(_minimum_issues(children, path)) + return issues + + +class Dpp4FunValidator: + """Validates D4.1 profile bindings without repeating generic core rules.""" + + def __init__(self) -> None: + self._resolver = Dpp4FunSemanticResolver() + + def validate(self, value: Dpp4Fun | None) -> ValidationResult: + issues: list[ValidationIssue] = [] + if value is None: + return ValidationResult.of([_issue("", "DPP4FUN_REQUIRED", "Dpp4Fun is required")]) + passport = value.passport + content_ids = passport.dpp_header.content_specification_ids + known_content_ids = { + DPP4FUN_PROFILE.metadata["contentSpecificationId"], + *DPP4FUN_PROFILE.variants.values(), + } + for content_id in content_ids: + if content_id not in known_content_ids: + issues.append( + _issue( + "contentSpecificationIds", + "DPP4FUN_CONTENT_SPEC_SUPPORTED", + f"unsupported content specification: {content_id}", + ) + ) + selected = [ + identifier + for identifier in DPP4FUN_PROFILE.variants.values() + if identifier in content_ids + ] + if len(selected) > 1: + issues.append( + _issue( + "contentSpecificationIds", + "DPP4FUN_CHARACTERISTICS_VARIANT_SINGLE", + "at most one Characteristics variant may be selected", + ) + ) + root_counts = Counter(element.element_id for element in passport.data_elements) + for element in passport.data_elements: + element_id = element.element_id or "" + path = SemanticPath.of(element_id) + if _definition(element_id, content_ids) is not None: + self._element(element, path, content_ids, f"elements.{element_id}", issues) + elif _defined_for_any_variant(element_id): + issues.append( + _issue( + f"elements.{element_id}", + "DPP4FUN_CHARACTERISTICS_VARIANT_MATCHES_CONTENT_SPEC", + "element is defined for a different Characteristics variant", + ) + ) + for root_id, count in root_counts.items(): + definition = _definition(root_id or "", content_ids) + if count > 1 and definition is not None: + issues.append( + _issue( + f"elements.{root_id}", + "DPP4FUN_EXPLICIT_CARDINALITY", + "catalog binding permits at most one root occurrence", + ) + ) + if "general" not in self._applicability(content_ids) and any( + _definition( + element.element_id or "", (DPP4FUN_PROFILE.metadata["contentSpecificationId"],) + ) + for element in passport.data_elements + ): + issues.append( + _issue( + "contentSpecificationIds", + "DPP4FUN_CONTENT_SPEC_SUPPORTED", + "general aspects require the DPP4Fun common content specification", + ) + ) + issues.extend(_minimum_issues(passport.data_elements)) + return ValidationResult.of(issues) + + @staticmethod + def _applicability(content_ids: Sequence[str]) -> set[str]: + result = ( + {"general"} + if DPP4FUN_PROFILE.metadata["contentSpecificationId"] in content_ids + else set() ) - if material.reference is not None: - _require_not_blank(material.reference, "Material.reference") - if material.mandatory and material.portion <= 0: - raise DppValidationError( - f"Material '{material.name}' is mandatory but has zero or no portion" + result.update( + f"characteristics-{variant}" + for variant, identifier in DPP4FUN_PROFILE.variants.items() + if identifier in content_ids ) - - -def validate_component(component: Component | None) -> None: - if component is None: - return - _require_not_blank(component.name, "Component.name") - if component.reference is not None: - _require_not_blank(component.reference, "Component.reference") - - -def validate_part(part: Part | None) -> None: - if part is None: - return - _require_not_blank(part.name, "Part.name") - if part.reference is not None: - _require_not_blank(part.reference, "Part.reference") - - -def _key(name: str, reference: str | None) -> str: - normalized_name = normalize_for_comparison(name) if name else "" - normalized_reference = normalize_for_comparison(reference) if reference else "" - return f"{normalized_name}|{normalized_reference}" - - -def validate_bill_of_materials(bom: BillOfMaterials | None) -> None: - if bom is None: + return result + + def _element( + self, + element: DataElement, + path: SemanticPath, + content_ids: Sequence[str], + issue_path: str, + issues: list[ValidationIssue], + ) -> None: + path_text = ".".join(path.segments) + binding = _definition(path_text, content_ids) + if binding is None: + if _defined_for_any_variant(path_text): + issues.append( + _issue( + issue_path, + "DPP4FUN_CHARACTERISTICS_VARIANT_MATCHES_CONTENT_SPEC", + "element is defined for a different Characteristics variant", + ) + ) + return + actual_kind = _KINDS[type(element).__name__] + expected_kind = _KINDS[binding.object_type] + if actual_kind != expected_kind: + issues.append( + _issue( + issue_path, + "DPP4FUN_RESERVED_PATH_KIND", + f"catalog expects {expected_kind} but found {actual_kind}", + ) + ) + return + definition = self._resolver.resolve(content_ids, path) + if ( + definition is not None + and definition.element_id is not None + and element.element_id != definition.element_id + ): + issues.append( + _issue( + issue_path, + "DPP4FUN_RESERVED_PATH_ELEMENT_ID", + "elementId does not match the catalog binding", + ) + ) + if ( + definition is not None + and definition.dictionary_reference_resolved + and element.dictionary_reference != definition.dictionary_reference + ): + issues.append( + _issue( + issue_path, + "DPP4FUN_RESERVED_PATH_DICTIONARY_REFERENCE", + "dictionaryReference does not match the catalog binding", + ) + ) + if isinstance(element, SingleValuedDataElement): + if ( + binding.unit_dimension + and binding.unit_dimension.endswith("-unit") + and _wrong_unit_dimension(element.value, binding.unit_dimension) + ): + issues.append( + _issue( + issue_path, + "DPP4FUN_UNIT_DIMENSION_MISMATCH", + "known unit symbol does not match the catalogued physical dimension", + ) + ) + if ( + definition is not None + and definition.value_data_type_resolved + and element.value_data_type != definition.value_data_type + ): + issues.append( + _issue( + issue_path, + "DPP4FUN_RESERVED_PATH_VALUE_DATATYPE", + "valueDataType does not match the catalog binding", + ) + ) + if not _value_matches(binding.value_data_type, element.value): + issues.append( + _issue( + issue_path, + "DPP4FUN_BINDING_DATATYPE_MISMATCH", + "value does not match the catalog datatype", + ) + ) + if ( + binding.value_data_type == "xsd:anyURI" + and isinstance(element.value, str) + and not _uri_syntax_valid(element.value) + ): + issues.append( + _issue( + f"{issue_path}.value", + "DPP4FUN_URI_FIELD_RFC3986", + "URI-valued D4.1 field must use RFC 3986 syntax", + ) + ) + elif isinstance(element, MultiValuedDataElement): + if ( + definition is not None + and definition.value_data_type_resolved + and element.value_data_type != definition.value_data_type + ): + issues.append( + _issue( + issue_path, + "DPP4FUN_RESERVED_PATH_VALUE_DATATYPE", + "valueDataType does not match the catalog binding", + ) + ) + if not element.value: + issues.append( + _issue( + issue_path, + "DPP4FUN_EXPLICIT_CARDINALITY", + "empty D4.1 lists must be omitted", + ) + ) + item_binding = _definition(f"{path_text}.0", content_ids) + source_count = ( + item_binding.source_cardinality if item_binding else binding.source_cardinality + ) + _check_count(source_count, len(element.value), issue_path, issues) + for index, child in enumerate(element.value): + item_path = path.child(str(index)) + item_path_text = ".".join(item_path.segments) + child_binding = _definition(item_path_text, content_ids) + if child_binding and child.object_type != child_binding.object_type: + issues.append( + _issue( + f"{issue_path}[{index}]", + "DPP4FUN_LIST_ITEM_KIND", + "repeated item must use the catalogued homogeneous child kind", + ) + ) + continue + if child_binding and child_binding.repeated_item_policy: + pattern = child_binding.repeated_item_policy.get("elementIdPattern") + if pattern and child.element_id != pattern % index: + issues.append( + _issue( + f"{issue_path}[{index}].elementId", + "DPP4FUN_LIST_ITEM_ID_CANONICAL", + "repeated item id does not match its canonical position", + ) + ) + self._element(child, item_path, content_ids, f"{issue_path}[{index}]", issues) + elif isinstance(element, DataElementCollection): + if not element.elements: + issues.append( + _issue( + issue_path, + "DPP4FUN_EXPLICIT_CARDINALITY", + "empty D4.1 collections must be omitted", + ) + ) + counts = Counter(child.element_id for child in element.elements) + for child in element.elements: + child_path = path.child(child.element_id or "") + child_binding = _definition(".".join(child_path.segments), content_ids) + maximum = child_binding.binding_occurrence.get("max") if child_binding else None + child_count = counts[child.element_id] + if maximum is not None and child_count > maximum: + issues.append( + _issue( + f"{issue_path}.{child.element_id}", + "DPP4FUN_EXPLICIT_CARDINALITY", + "catalog binding occurrence is exceeded", + ) + ) + self._element( + child, child_path, content_ids, f"{issue_path}.{child.element_id}", issues + ) + elif isinstance(element, (MultiLanguageDataElement, RelatedResource)): + return + + +def _check_count( + occurrence: Mapping[str, int | None] | None, + count: int, + path: str, + issues: list[ValidationIssue], +) -> None: + if occurrence is None: return - - material_keys: set[str] = set() - for index, material in enumerate(bom.materials): - if material is None: - raise DppValidationError(f"BillOfMaterials.materials[{index}] must not be null") - validate_material(material) - key = _key(material.name, material.reference) - if key in material_keys: - raise DppValidationError(f"BillOfMaterials.materials contains duplicate entry: {key}") - material_keys.add(key) - - component_keys: set[str] = set() - for index, component in enumerate(bom.components): - if component is None: - raise DppValidationError(f"BillOfMaterials.components[{index}] must not be null") - validate_component(component) - key = _key(component.name, component.reference) - if key in component_keys: - raise DppValidationError(f"BillOfMaterials.components contains duplicate entry: {key}") - component_keys.add(key) - - part_keys: set[str] = set() - for index, part in enumerate(bom.parts): - if part is None: - raise DppValidationError(f"BillOfMaterials.parts[{index}] must not be null") - validate_part(part) - key = _key(part.name, part.reference) - if key in part_keys: - raise DppValidationError(f"BillOfMaterials.parts contains duplicate entry: {key}") - part_keys.add(key) - - -def validate_product_classification(classification: ProductClassification | None) -> None: - if classification is None: - raise DppValidationError("ProductClassification is required") - _require_not_blank(classification.sector, "ProductClassification.sector") - _require_not_blank(classification.category, "ProductClassification.category") - if classification.group is not None: - _require_not_blank(classification.group, "ProductClassification.group") - if classification.subCategory is not None: - _require_not_blank(classification.subCategory, "ProductClassification.subCategory") - - if _has_text(classification.subCategory) and not _has_text(classification.category): - raise DppValidationError("ProductClassification.subCategory is set but category is missing") - if _has_text(classification.group) and not _has_text(classification.sector): - raise DppValidationError("ProductClassification.group is set but sector is missing") - - if classification.tags: - _require_clean_string_list(classification.tags, "ProductClassification.tags") - - -def validate_characteristics(characteristics: Characteristics | None) -> None: - if characteristics is None: - raise DppValidationError("Characteristics is required") - _require_not_blank(characteristics.productName, "Characteristics.productName") - if characteristics.weight is not None and ( - not isfinite(characteristics.weight) or characteristics.weight < 0 - ): - raise DppValidationError( - "Characteristics.weight must be finite and non-negative, but got " - f"{characteristics.weight}" - ) - validate_dimensions(characteristics.dimensions) - if characteristics.features: - _require_clean_string_list(characteristics.features, "Characteristics.features") - - -def validate_dpp4fun(dpp: Dpp4Fun | None) -> None: - """Validate a complete furniture DPP. Fail-fast; raises on first violation.""" - if dpp is None: - raise DppValidationError("Dpp4Fun cannot be null") - - validate_dpp_core(dpp.coreDpp) - validate_product_classification(dpp.classification) - validate_characteristics(dpp.characteristics) - validate_bill_of_materials(dpp.billOfMaterials) - - _validate_cross_rules(dpp) - - -def _validate_cross_rules(dpp: Dpp4Fun) -> None: - category = dpp.category - product_type = dpp.productType - if _has_text(category) and _has_text(product_type): - cat_lower = normalize_for_comparison(category) - type_lower = normalize_for_comparison(product_type) # type: ignore[arg-type] - if cat_lower not in type_lower and type_lower not in cat_lower: - raise DppValidationError( - f"Cross-object validation: classification.category '{category}' " - f"and characteristics.productType '{product_type}' appear inconsistent" + minimum = occurrence.get("min") or 0 + maximum = occurrence.get("max") + if count < minimum or maximum is not None and count > maximum: + issues.append( + _issue( + path, + "DPP4FUN_EXPLICIT_CARDINALITY", + "item count conflicts with frozen source cardinality", ) - - if dpp.documentation is None and _has_text(dpp.externalDocumentationLink): - raise DppValidationError( - "Cross-object validation: PassportMetadata has an externalDocumentationLink " - "but no Documentation object is present in the DPP" ) + + +class Dpp4FunValidationService: + """Java-equivalent ordered validation composition.""" + + def __init__(self) -> None: + self._core = DppValidator() + self._format = DataFormatValidator() + self._identifier = IdentifierValidator() + self._profile = Dpp4FunValidator() + + def validate( + self, value: Dpp4Fun | None, context: IdentifierValidationContext | None = None + ) -> ValidationResult: + if value is None: + return self._profile.validate(None) + passport = value.passport + issues = list(self._core.validate(passport).issues) + issues.extend(self._format.validate(passport).issues) + if context is not None: + issues.extend(self._identifier.validate(passport.dpp_header, context).issues) + issues.extend(self._profile.validate(value).issues) + return ValidationResult.of(issues) + + def validate_profile(self, value: Dpp4Fun | None) -> ValidationResult: + return self._profile.validate(value) + + +def validate_dpp4fun( + value: Dpp4Fun | None, context: IdentifierValidationContext | None = None +) -> ValidationResult: + """Return ordered core, format, optional identifier, and D4.1 findings.""" + return Dpp4FunValidationService().validate(value, context) diff --git a/tests/conftest.py b/tests/conftest.py index a1c4956..e056642 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,31 +2,8 @@ from __future__ import annotations -from datetime import date -from uuid import UUID - import pytest -from dpp_sdk.core.model import ( - Address, - Contact, - Documentation, - DppCore, - Email, - Nameplate, - Organization, - OrganizationRole, - PassportMetadata, -) -from dpp_sdk.dpp4fun.model import ( - BillOfMaterials, - Characteristics, - Dimensions, - Dpp4Fun, - Material, - ProductClassification, -) - def pytest_addoption(parser: pytest.Parser) -> None: try: @@ -53,65 +30,3 @@ def require_mock_services_for_integration( "--run-mock-services" ): pytest.skip("requires --run-mock-services and running mock services") - - -@pytest.fixture -def manufacturer() -> Organization: - return Organization( - name="ACME Furniture GmbH", - gln="4012345000009", - role=OrganizationRole.MANUFACTURER, - contact=Contact( - organization="ACME HQ", - address=Address(country="DE", town="Berlin", street="Hauptstr. 1", zipCode="10115"), - email=Email(emailAddress="info@acme.example", typeOfEmail="business"), - ), - ) - - -@pytest.fixture -def valid_core(manufacturer: Organization) -> DppCore: - return DppCore( - passportMetadata=PassportMetadata( - uniqueProductIdentifier=UUID("11111111-1111-1111-1111-111111111111"), - passportUpdateDates=[date(2024, 1, 1)], - qrCodeOrDigitalTag="QR-001", - ), - nameplate=Nameplate( - gtinCode="GTIN-0001", - internalArticleNumber="ART-1", - manufacturer=manufacturer, - ), - documentation=Documentation( - digitalInstructionsLink="https://acme.example/docs", - downloadable=True, - availableForYears=10, - ), - ) - - -@pytest.fixture -def valid_dpp4fun(valid_core: DppCore) -> Dpp4Fun: - return Dpp4Fun( - coreDpp=valid_core, - classification=ProductClassification( - sector="Furniture", - category="Office Chair", - group="Seating", - tags=["ergonomic", "adjustable"], - ), - characteristics=Characteristics( - productName="ErgoChair Pro", - productType="Office Chair", - brand="ACME", - dimensions=Dimensions(width=60.0, height=120.0, depth=60.0, unit="cm"), - weight=14.5, - features=["lumbar-support"], - ), - billOfMaterials=BillOfMaterials( - materials=[ - Material(name="steel", mandatory=True, portion=0.6, reference="MAT-STEEL"), - Material(name="foam", portion=0.4), - ], - ), - ) diff --git a/tests/fixtures/java-contract/README.md b/tests/fixtures/java-contract/README.md deleted file mode 100644 index 5188150..0000000 --- a/tests/fixtures/java-contract/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# Java contract fixture snapshot - -These immutable fixtures are owned by the Python test suite. They were generated from the Java -`dpp-datamodel/dpp4fun` implementation at commit -`9933d674ba27cd987f1bba731eb57b8dbb6bba95`. The original Java SHA-256 remains -in `sha256`; `pythonSnapshotSha256` protects the repository-owned LF-normalized copy. The root -`.gitattributes` keeps those bytes stable across operating systems. - -Ordinary Python tests read only this directory. They do not require a Java checkout, Maven, parent -workspace evidence, or `docs/java-python-parity`. Regenerating or replacing the snapshot is a -separate cross-repository governance task; reviewing such a replacement must verify the source -commit, generator, fixture bytes, hashes, and expected outcomes before this directory is updated. diff --git a/tests/fixtures/java-contract/canonical-java-output.json b/tests/fixtures/java-contract/canonical-java-output.json deleted file mode 100644 index a4d1ab7..0000000 --- a/tests/fixtures/java-contract/canonical-java-output.json +++ /dev/null @@ -1 +0,0 @@ -{"classification":{"sector":"Furniture","group":"Seating","category":"Bed","subCategory":"Frame","tags":["fixture"]},"characteristics":{"productName":"Deterministic Fixture Bed","description":"Java-generated contract fixture","brand":"Dpp4Fun","productType":"Bed","dimensions":{"width":200.0,"height":90.0,"depth":50.0,"unit":"cm"},"weight":75.0,"color":"Oak","features":["fixed-feature"]},"billOfMaterials":{"materials":[{"name":"Steel","mandatory":true,"portion":2.5,"reference":"MAT-FIXTURE-001"}],"components":[{"name":"Frame","reference":"COMP-FIXTURE-001"}],"parts":[{"name":"Support","mandatory":true,"reference":"PART-FIXTURE-001"}]},"passportMetadata":{"uniqueProductIdentifier":"11111111-1111-4111-8111-111111111111","passportUpdateDates":["2026-01-15"],"qrCodeOrDigitalTag":"QR-FIXTURE-001","externalDocumentationLink":"https://example.com/dpp-doc"},"nameplate":{"gtinCode":"GTIN-FIXTURE-001","internalArticleNumber":"ART-FIXTURE-001","batchNumber":"BATCH-2026-01","customsTariffNumber":null,"uriOfTheProduct":null,"manufacturer":{"name":"Fixture Manufacturer GmbH","gln":"123456789","productDescription":null,"productDesignation":null,"productFamily":null,"productRoot":null,"productOrderSuffix":null,"uri":"https://manufacturer.example.com","contact":null,"role":"MANUFACTURER"},"supplier":null},"documentation":{"digitalInstructionsLink":"https://example.com/instructions.pdf","safetyInstructionsLink":"https://example.com/safety.pdf","downloadable":true,"availableForYears":10,"paperCopyAvailableOnRequest":false}} diff --git a/tests/fixtures/java-contract/fixture-metadata.json b/tests/fixtures/java-contract/fixture-metadata.json deleted file mode 100644 index b1de9e2..0000000 --- a/tests/fixtures/java-contract/fixture-metadata.json +++ /dev/null @@ -1,161 +0,0 @@ -{ - "schemaVersion" : "1.0", - "sourceCommit" : "9933d674ba27cd987f1bba731eb57b8dbb6bba95", - "module" : "dpp-datamodel/dpp4fun", - "generatorClass" : "DeterministicFixtureGenerator", - "productionClassesExecuted" : [ "Dpp4Fun.Builder", "DppCore.Builder", "Dpp4FunValidationService", "Dpp4FunJsonCodec", "Jackson ObjectMapper" ], - "fixtures" : [ { - "fixtureId" : "models:valid-complete-dpp", - "file" : "valid-complete-dpp.json", - "sourceCommit" : "9933d674ba27cd987f1bba731eb57b8dbb6bba95", - "module" : "dpp-datamodel/dpp4fun", - "generatorClass" : "DeterministicFixtureGenerator", - "generatorMethod" : "completeDpp", - "productionClassesExecuted" : [ "Dpp4Fun.Builder", "Dpp4FunValidationService", "Dpp4FunJsonCodec" ], - "deterministicValues" : { - "uuid" : "11111111-1111-4111-8111-111111111111", - "date" : "2026-01-15" - }, - "expectedOutcome" : "success", - "expectedException" : null, - "sha256" : "031cb4f28bfd595da92304d53b2d9f663604208627c6f14ea3e4597267b4aad3", - "pythonSnapshotSha256" : "4e50b877096e44fb2162dbf62c9ab5f14e8350f5fc69e4d7fe12817ec5f3a93f", - "generationCommand" : "powershell.exe -ExecutionPolicy Bypass -File generate-deterministic-fixtures.ps1" - }, { - "fixtureId" : "models:valid-minimal-dpp", - "file" : "valid-minimal-dpp.json", - "sourceCommit" : "9933d674ba27cd987f1bba731eb57b8dbb6bba95", - "module" : "dpp-datamodel/dpp4fun", - "generatorClass" : "DeterministicFixtureGenerator", - "generatorMethod" : "minimalDpp", - "productionClassesExecuted" : [ "Dpp4Fun.Builder", "Dpp4FunValidationService", "Dpp4FunJsonCodec" ], - "deterministicValues" : { - "uuid" : "11111111-1111-4111-8111-111111111111", - "date" : "2026-01-15" - }, - "expectedOutcome" : "success", - "expectedException" : null, - "sha256" : "b6ff2aa61d337cb5d1098d577e71d95e8dbfc169563dc809e6f1d491ac8da5e5", - "pythonSnapshotSha256" : "6160b937e4d01453d1ae530cd985689de5e0f110241345c519d1684198ace18d", - "generationCommand" : "powershell.exe -ExecutionPolicy Bypass -File generate-deterministic-fixtures.ps1" - }, { - "fixtureId" : "models:canonical-java-output", - "file" : "canonical-java-output.json", - "sourceCommit" : "9933d674ba27cd987f1bba731eb57b8dbb6bba95", - "module" : "dpp-datamodel/dpp4fun", - "generatorClass" : "DeterministicFixtureGenerator", - "generatorMethod" : "completeDpp", - "productionClassesExecuted" : [ "Dpp4Fun.Builder", "Dpp4FunValidationService", "Dpp4FunJsonCodec" ], - "deterministicValues" : { - "uuid" : "11111111-1111-4111-8111-111111111111", - "date" : "2026-01-15" - }, - "expectedOutcome" : "success", - "expectedException" : null, - "sha256" : "031cb4f28bfd595da92304d53b2d9f663604208627c6f14ea3e4597267b4aad3", - "pythonSnapshotSha256" : "4e50b877096e44fb2162dbf62c9ab5f14e8350f5fc69e4d7fe12817ec5f3a93f", - "generationCommand" : "powershell.exe -ExecutionPolicy Bypass -File generate-deterministic-fixtures.ps1" - }, { - "fixtureId" : "models:valid-flat-input", - "file" : "valid-flat-input.json", - "sourceCommit" : "9933d674ba27cd987f1bba731eb57b8dbb6bba95", - "module" : "dpp-datamodel/dpp4fun", - "generatorClass" : "DeterministicFixtureGenerator", - "generatorMethod" : "completeDpp", - "productionClassesExecuted" : [ "Dpp4Fun.Builder", "Dpp4FunValidationService", "Dpp4FunJsonCodec" ], - "deterministicValues" : { - "uuid" : "11111111-1111-4111-8111-111111111111", - "date" : "2026-01-15" - }, - "expectedOutcome" : "success", - "expectedException" : null, - "sha256" : "031cb4f28bfd595da92304d53b2d9f663604208627c6f14ea3e4597267b4aad3", - "pythonSnapshotSha256" : "4e50b877096e44fb2162dbf62c9ab5f14e8350f5fc69e4d7fe12817ec5f3a93f", - "generationCommand" : "powershell.exe -ExecutionPolicy Bypass -File generate-deterministic-fixtures.ps1" - }, { - "fixtureId" : "models:valid-nested-input", - "file" : "valid-nested-input.json", - "sourceCommit" : "9933d674ba27cd987f1bba731eb57b8dbb6bba95", - "module" : "dpp-datamodel/dpp4fun", - "generatorClass" : "DeterministicFixtureGenerator", - "generatorMethod" : "nestedInput", - "productionClassesExecuted" : [ "Dpp4Fun.Builder", "Dpp4FunValidationService", "Dpp4FunJsonCodec" ], - "deterministicValues" : { - "uuid" : "11111111-1111-4111-8111-111111111111", - "date" : "2026-01-15" - }, - "expectedOutcome" : "success", - "expectedException" : null, - "sha256" : "7fb0533e1f0b144deca0efced5c7044cec4f879fd729d13113a0b2a3a5f217fb", - "pythonSnapshotSha256" : "1f16c92b55c421cf649dbf63fba1ac8c77180b5ae542705e9d4a40a1d1149d24", - "generationCommand" : "powershell.exe -ExecutionPolicy Bypass -File generate-deterministic-fixtures.ps1" - }, { - "fixtureId" : "errors:invalid-missing-required-field", - "file" : "invalid-missing-required-field.json", - "sourceCommit" : "9933d674ba27cd987f1bba731eb57b8dbb6bba95", - "module" : "dpp-datamodel/dpp4fun", - "generatorClass" : "DeterministicFixtureGenerator", - "generatorMethod" : "missingRequired", - "productionClassesExecuted" : [ "Dpp4Fun.Builder", "Dpp4FunValidationService", "Dpp4FunJsonCodec" ], - "deterministicValues" : { - "uuid" : "11111111-1111-4111-8111-111111111111", - "date" : "2026-01-15" - }, - "expectedOutcome" : "failure", - "expectedException" : "MappingException", - "sha256" : "7c92f22e1013c95dbd222930fb5f81dcae38c375d80697a03e16e8df559a9e9e", - "pythonSnapshotSha256" : "b7e550f12f0ab45ae2adefc4d7771704b47f143c2aaf428707968235573a3cf2", - "generationCommand" : "powershell.exe -ExecutionPolicy Bypass -File generate-deterministic-fixtures.ps1" - }, { - "fixtureId" : "errors:invalid-semantic-rule", - "file" : "invalid-semantic-rule.json", - "sourceCommit" : "9933d674ba27cd987f1bba731eb57b8dbb6bba95", - "module" : "dpp-datamodel/dpp4fun", - "generatorClass" : "DeterministicFixtureGenerator", - "generatorMethod" : "semanticFailure", - "productionClassesExecuted" : [ "Dpp4Fun.Builder", "Dpp4FunValidationService", "Dpp4FunJsonCodec" ], - "deterministicValues" : { - "uuid" : "11111111-1111-4111-8111-111111111111", - "date" : "2026-01-15" - }, - "expectedOutcome" : "failure", - "expectedException" : "ValidationException", - "sha256" : "73db902b95a48ad2859289c97fb35e662ad711febe1e21593d639882e4c4dad1", - "pythonSnapshotSha256" : "21f45231f7e194fc8ebeaadcbb4d49caa35efe609579900be12c2eeb228cda2c", - "generationCommand" : "powershell.exe -ExecutionPolicy Bypass -File generate-deterministic-fixtures.ps1" - }, { - "fixtureId" : "errors:invalid-cross-object-rule", - "file" : "invalid-cross-object-rule.json", - "sourceCommit" : "9933d674ba27cd987f1bba731eb57b8dbb6bba95", - "module" : "dpp-datamodel/dpp4fun", - "generatorClass" : "DeterministicFixtureGenerator", - "generatorMethod" : "crossObjectFailure", - "productionClassesExecuted" : [ "Dpp4Fun.Builder", "Dpp4FunValidationService", "Dpp4FunJsonCodec" ], - "deterministicValues" : { - "uuid" : "11111111-1111-4111-8111-111111111111", - "date" : "2026-01-15" - }, - "expectedOutcome" : "failure", - "expectedException" : "ValidationException", - "sha256" : "6812187eb825ce6da4a10cfb96c6c7e56a8c36249172bc896a991285ea477979", - "pythonSnapshotSha256" : "dff4dd4a33079535812ba92882fba598561ab9be7a2008e924bc043bf545d04d", - "generationCommand" : "powershell.exe -ExecutionPolicy Bypass -File generate-deterministic-fixtures.ps1" - }, { - "fixtureId" : "errors:malformed-json", - "file" : "malformed-json.txt", - "sourceCommit" : "9933d674ba27cd987f1bba731eb57b8dbb6bba95", - "module" : "dpp-datamodel/dpp4fun", - "generatorClass" : "DeterministicFixtureGenerator", - "generatorMethod" : "malformedJson", - "productionClassesExecuted" : [ "Dpp4Fun.Builder", "Dpp4FunValidationService", "Dpp4FunJsonCodec" ], - "deterministicValues" : { - "uuid" : "11111111-1111-4111-8111-111111111111", - "date" : "2026-01-15" - }, - "expectedOutcome" : "failure", - "expectedException" : "IllegalArgumentException", - "sha256" : "fc1932df6fdd4cdd812e28ae28bc9b5f98df11f0e81fa699d4440e1a136edbb7", - "pythonSnapshotSha256" : "5715ea11351f96c1faee52d83ceb8edf41612427a452f7acc780b6bc05072500", - "generationCommand" : "powershell.exe -ExecutionPolicy Bypass -File generate-deterministic-fixtures.ps1" - } ] -} diff --git a/tests/fixtures/java-contract/invalid-cross-object-rule.json b/tests/fixtures/java-contract/invalid-cross-object-rule.json deleted file mode 100644 index 0c72109..0000000 --- a/tests/fixtures/java-contract/invalid-cross-object-rule.json +++ /dev/null @@ -1 +0,0 @@ -{"classification":{"sector":"Furniture","group":"Seating","category":"Bed","subCategory":"Frame","tags":["fixture"]},"characteristics":{"productName":"Deterministic Fixture Bed","description":"Java-generated contract fixture","brand":"Dpp4Fun","productType":"Bed","dimensions":{"width":200.0,"height":90.0,"depth":50.0,"unit":"cm"},"weight":75.0,"color":"Oak","features":["fixed-feature"]},"billOfMaterials":{"materials":[{"name":"Steel","mandatory":true,"portion":2.5,"reference":"MAT-FIXTURE-001"}],"components":[{"name":"Frame","reference":"COMP-FIXTURE-001"}],"parts":[{"name":"Support","mandatory":true,"reference":"PART-FIXTURE-001"}]},"passportMetadata":{"uniqueProductIdentifier":"11111111-1111-4111-8111-111111111111","passportUpdateDates":["2026-01-15"],"qrCodeOrDigitalTag":"QR-FIXTURE-001","externalDocumentationLink":"https://example.com/dpp-doc"},"nameplate":{"gtinCode":"GTIN-FIXTURE-001","internalArticleNumber":"ART-FIXTURE-001","batchNumber":"BATCH-2026-01","customsTariffNumber":null,"uriOfTheProduct":null,"manufacturer":{"name":"Fixture Manufacturer GmbH","gln":"123456789","productDescription":null,"productDesignation":null,"productFamily":null,"productRoot":null,"productOrderSuffix":null,"uri":"https://manufacturer.example.com","contact":null,"role":"SUPPLIER"},"supplier":null},"documentation":{"digitalInstructionsLink":"https://example.com/instructions.pdf","safetyInstructionsLink":"https://example.com/safety.pdf","downloadable":true,"availableForYears":10,"paperCopyAvailableOnRequest":false}} diff --git a/tests/fixtures/java-contract/invalid-missing-required-field.json b/tests/fixtures/java-contract/invalid-missing-required-field.json deleted file mode 100644 index f7e0ae9..0000000 --- a/tests/fixtures/java-contract/invalid-missing-required-field.json +++ /dev/null @@ -1 +0,0 @@ -{"classification":{"sector":"Furniture","group":"Seating","category":"Bed","subCategory":"Frame","tags":["fixture"]},"characteristics":{"productName":"Deterministic Fixture Bed","description":"Java-generated contract fixture","brand":"Dpp4Fun","productType":"Bed","dimensions":{"width":200.0,"height":90.0,"depth":50.0,"unit":"cm"},"weight":75.0,"color":"Oak","features":["fixed-feature"]},"billOfMaterials":{"materials":[{"name":"Steel","mandatory":true,"portion":2.5,"reference":"MAT-FIXTURE-001"}],"components":[{"name":"Frame","reference":"COMP-FIXTURE-001"}],"parts":[{"name":"Support","mandatory":true,"reference":"PART-FIXTURE-001"}]},"passportMetadata":{"uniqueProductIdentifier":"11111111-1111-4111-8111-111111111111","passportUpdateDates":["2026-01-15"],"qrCodeOrDigitalTag":"QR-FIXTURE-001","externalDocumentationLink":"https://example.com/dpp-doc"},"documentation":{"digitalInstructionsLink":"https://example.com/instructions.pdf","safetyInstructionsLink":"https://example.com/safety.pdf","downloadable":true,"availableForYears":10,"paperCopyAvailableOnRequest":false}} diff --git a/tests/fixtures/java-contract/invalid-semantic-rule.json b/tests/fixtures/java-contract/invalid-semantic-rule.json deleted file mode 100644 index 061c087..0000000 --- a/tests/fixtures/java-contract/invalid-semantic-rule.json +++ /dev/null @@ -1 +0,0 @@ -{"classification":{"sector":"Furniture","group":"Seating","category":"Bed","subCategory":"Frame","tags":["fixture"]},"characteristics":{"productName":"Deterministic Fixture Bed","description":"Java-generated contract fixture","brand":"Dpp4Fun","productType":"Chair","dimensions":{"width":200.0,"height":90.0,"depth":50.0,"unit":"cm"},"weight":75.0,"color":"Oak","features":["fixed-feature"]},"billOfMaterials":{"materials":[{"name":"Steel","mandatory":true,"portion":2.5,"reference":"MAT-FIXTURE-001"}],"components":[{"name":"Frame","reference":"COMP-FIXTURE-001"}],"parts":[{"name":"Support","mandatory":true,"reference":"PART-FIXTURE-001"}]},"passportMetadata":{"uniqueProductIdentifier":"11111111-1111-4111-8111-111111111111","passportUpdateDates":["2026-01-15"],"qrCodeOrDigitalTag":"QR-FIXTURE-001","externalDocumentationLink":"https://example.com/dpp-doc"},"nameplate":{"gtinCode":"GTIN-FIXTURE-001","internalArticleNumber":"ART-FIXTURE-001","batchNumber":"BATCH-2026-01","customsTariffNumber":null,"uriOfTheProduct":null,"manufacturer":{"name":"Fixture Manufacturer GmbH","gln":"123456789","productDescription":null,"productDesignation":null,"productFamily":null,"productRoot":null,"productOrderSuffix":null,"uri":"https://manufacturer.example.com","contact":null,"role":"MANUFACTURER"},"supplier":null},"documentation":{"digitalInstructionsLink":"https://example.com/instructions.pdf","safetyInstructionsLink":"https://example.com/safety.pdf","downloadable":true,"availableForYears":10,"paperCopyAvailableOnRequest":false}} diff --git a/tests/fixtures/java-contract/legacy-blocker-placeholders.json b/tests/fixtures/java-contract/legacy-blocker-placeholders.json deleted file mode 100644 index ccc5ec6..0000000 --- a/tests/fixtures/java-contract/legacy-blocker-placeholders.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "preservedAtCommit" : "9933d674ba27cd987f1bba731eb57b8dbb6bba95", - "classification" : "BLOCKER_PLACEHOLDER", - "files" : { - "canonical-java-output.json" : "{\"fixtureStatus\":\"BLOCKED\",\"fixtureName\":\"canonical-java-output.json\",\"reason\":\"No deterministic Java producer exists; see golden-fixtures/README.md and UNC-003.\"}\n", - "invalid-cross-object-rule.json" : "{\"fixtureStatus\":\"BLOCKED\",\"fixtureName\":\"invalid-cross-object-rule.json\",\"reason\":\"No deterministic Java producer exists; see golden-fixtures/README.md and UNC-003.\"}\n", - "invalid-missing-required-field.json" : "{\"fixtureStatus\":\"BLOCKED\",\"fixtureName\":\"invalid-missing-required-field.json\",\"reason\":\"No deterministic Java producer exists; see golden-fixtures/README.md and UNC-003.\"}\n", - "invalid-semantic-rule.json" : "{\"fixtureStatus\":\"BLOCKED\",\"fixtureName\":\"invalid-semantic-rule.json\",\"reason\":\"No deterministic Java producer exists; see golden-fixtures/README.md and UNC-003.\"}\n", - "valid-complete-dpp.json" : "{\"fixtureStatus\":\"BLOCKED\",\"fixtureName\":\"valid-complete-dpp.json\",\"reason\":\"No deterministic Java producer exists; see golden-fixtures/README.md and UNC-003.\"}\n", - "valid-flat-input.json" : "{\"fixtureStatus\":\"BLOCKED\",\"fixtureName\":\"valid-flat-input.json\",\"reason\":\"No deterministic Java producer exists; see golden-fixtures/README.md and UNC-003.\"}\n", - "valid-minimal-dpp.json" : "{\"fixtureStatus\":\"BLOCKED\",\"fixtureName\":\"valid-minimal-dpp.json\",\"reason\":\"No deterministic Java producer exists; see golden-fixtures/README.md and UNC-003.\"}\n", - "valid-nested-input.json" : "{\"fixtureStatus\":\"BLOCKED\",\"fixtureName\":\"valid-nested-input.json\",\"reason\":\"No deterministic Java producer exists; see golden-fixtures/README.md and UNC-003.\"}\n", - "malformed-json.txt" : "BLOCKED: no persisted Java-generated malformed-input fixture. Evidence source is TestDataFactory.createMalformedJson() and Dpp4FunJsonCodecTest; see UNC-003.\n" - } -} diff --git a/tests/fixtures/java-contract/malformed-json.txt b/tests/fixtures/java-contract/malformed-json.txt deleted file mode 100644 index 89d4d25..0000000 --- a/tests/fixtures/java-contract/malformed-json.txt +++ /dev/null @@ -1 +0,0 @@ -{not-json diff --git a/tests/fixtures/java-contract/valid-complete-dpp.json b/tests/fixtures/java-contract/valid-complete-dpp.json deleted file mode 100644 index a4d1ab7..0000000 --- a/tests/fixtures/java-contract/valid-complete-dpp.json +++ /dev/null @@ -1 +0,0 @@ -{"classification":{"sector":"Furniture","group":"Seating","category":"Bed","subCategory":"Frame","tags":["fixture"]},"characteristics":{"productName":"Deterministic Fixture Bed","description":"Java-generated contract fixture","brand":"Dpp4Fun","productType":"Bed","dimensions":{"width":200.0,"height":90.0,"depth":50.0,"unit":"cm"},"weight":75.0,"color":"Oak","features":["fixed-feature"]},"billOfMaterials":{"materials":[{"name":"Steel","mandatory":true,"portion":2.5,"reference":"MAT-FIXTURE-001"}],"components":[{"name":"Frame","reference":"COMP-FIXTURE-001"}],"parts":[{"name":"Support","mandatory":true,"reference":"PART-FIXTURE-001"}]},"passportMetadata":{"uniqueProductIdentifier":"11111111-1111-4111-8111-111111111111","passportUpdateDates":["2026-01-15"],"qrCodeOrDigitalTag":"QR-FIXTURE-001","externalDocumentationLink":"https://example.com/dpp-doc"},"nameplate":{"gtinCode":"GTIN-FIXTURE-001","internalArticleNumber":"ART-FIXTURE-001","batchNumber":"BATCH-2026-01","customsTariffNumber":null,"uriOfTheProduct":null,"manufacturer":{"name":"Fixture Manufacturer GmbH","gln":"123456789","productDescription":null,"productDesignation":null,"productFamily":null,"productRoot":null,"productOrderSuffix":null,"uri":"https://manufacturer.example.com","contact":null,"role":"MANUFACTURER"},"supplier":null},"documentation":{"digitalInstructionsLink":"https://example.com/instructions.pdf","safetyInstructionsLink":"https://example.com/safety.pdf","downloadable":true,"availableForYears":10,"paperCopyAvailableOnRequest":false}} diff --git a/tests/fixtures/java-contract/valid-flat-input.json b/tests/fixtures/java-contract/valid-flat-input.json deleted file mode 100644 index a4d1ab7..0000000 --- a/tests/fixtures/java-contract/valid-flat-input.json +++ /dev/null @@ -1 +0,0 @@ -{"classification":{"sector":"Furniture","group":"Seating","category":"Bed","subCategory":"Frame","tags":["fixture"]},"characteristics":{"productName":"Deterministic Fixture Bed","description":"Java-generated contract fixture","brand":"Dpp4Fun","productType":"Bed","dimensions":{"width":200.0,"height":90.0,"depth":50.0,"unit":"cm"},"weight":75.0,"color":"Oak","features":["fixed-feature"]},"billOfMaterials":{"materials":[{"name":"Steel","mandatory":true,"portion":2.5,"reference":"MAT-FIXTURE-001"}],"components":[{"name":"Frame","reference":"COMP-FIXTURE-001"}],"parts":[{"name":"Support","mandatory":true,"reference":"PART-FIXTURE-001"}]},"passportMetadata":{"uniqueProductIdentifier":"11111111-1111-4111-8111-111111111111","passportUpdateDates":["2026-01-15"],"qrCodeOrDigitalTag":"QR-FIXTURE-001","externalDocumentationLink":"https://example.com/dpp-doc"},"nameplate":{"gtinCode":"GTIN-FIXTURE-001","internalArticleNumber":"ART-FIXTURE-001","batchNumber":"BATCH-2026-01","customsTariffNumber":null,"uriOfTheProduct":null,"manufacturer":{"name":"Fixture Manufacturer GmbH","gln":"123456789","productDescription":null,"productDesignation":null,"productFamily":null,"productRoot":null,"productOrderSuffix":null,"uri":"https://manufacturer.example.com","contact":null,"role":"MANUFACTURER"},"supplier":null},"documentation":{"digitalInstructionsLink":"https://example.com/instructions.pdf","safetyInstructionsLink":"https://example.com/safety.pdf","downloadable":true,"availableForYears":10,"paperCopyAvailableOnRequest":false}} diff --git a/tests/fixtures/java-contract/valid-minimal-dpp.json b/tests/fixtures/java-contract/valid-minimal-dpp.json deleted file mode 100644 index 9ad0f28..0000000 --- a/tests/fixtures/java-contract/valid-minimal-dpp.json +++ /dev/null @@ -1 +0,0 @@ -{"classification":{"sector":"Furniture","group":null,"category":"Bed","subCategory":null,"tags":[]},"characteristics":{"productName":"Minimal Fixture Bed","description":null,"brand":null,"productType":"Bed","dimensions":null,"weight":null,"color":null,"features":[]},"billOfMaterials":null,"passportMetadata":{"uniqueProductIdentifier":"11111111-1111-4111-8111-111111111111","passportUpdateDates":["2026-01-15"],"qrCodeOrDigitalTag":null,"externalDocumentationLink":null},"nameplate":{"gtinCode":"GTIN-FIXTURE-MIN","internalArticleNumber":null,"batchNumber":null,"customsTariffNumber":null,"uriOfTheProduct":null,"manufacturer":{"name":"Fixture Manufacturer GmbH","gln":"123456789","productDescription":null,"productDesignation":null,"productFamily":null,"productRoot":null,"productOrderSuffix":null,"uri":null,"contact":null,"role":"MANUFACTURER"},"supplier":null},"documentation":null} diff --git a/tests/fixtures/java-contract/valid-nested-input.json b/tests/fixtures/java-contract/valid-nested-input.json deleted file mode 100644 index 928be8f..0000000 --- a/tests/fixtures/java-contract/valid-nested-input.json +++ /dev/null @@ -1 +0,0 @@ -{"classification":{"sector":"Furniture","group":"Seating","category":"Bed","subCategory":"Frame","tags":["fixture"]},"characteristics":{"productName":"Deterministic Fixture Bed","description":"Java-generated contract fixture","brand":"Dpp4Fun","productType":"Bed","dimensions":{"width":200.0,"height":90.0,"depth":50.0,"unit":"cm"},"weight":75.0,"color":"Oak","features":["fixed-feature"]},"billOfMaterials":{"materials":[{"name":"Steel","mandatory":true,"portion":2.5,"reference":"MAT-FIXTURE-001"}],"components":[{"name":"Frame","reference":"COMP-FIXTURE-001"}],"parts":[{"name":"Support","mandatory":true,"reference":"PART-FIXTURE-001"}]},"coreDpp":{"passportMetadata":{"uniqueProductIdentifier":"11111111-1111-4111-8111-111111111111","passportUpdateDates":["2026-01-15"],"qrCodeOrDigitalTag":"QR-FIXTURE-001","externalDocumentationLink":"https://example.com/dpp-doc"},"nameplate":{"gtinCode":"GTIN-FIXTURE-001","internalArticleNumber":"ART-FIXTURE-001","batchNumber":"BATCH-2026-01","customsTariffNumber":null,"uriOfTheProduct":null,"manufacturer":{"name":"Fixture Manufacturer GmbH","gln":"123456789","productDescription":null,"productDesignation":null,"productFamily":null,"productRoot":null,"productOrderSuffix":null,"uri":"https://manufacturer.example.com","contact":null,"role":"MANUFACTURER"},"supplier":null},"documentation":{"digitalInstructionsLink":"https://example.com/instructions.pdf","safetyInstructionsLink":"https://example.com/safety.pdf","downloadable":true,"availableForYears":10,"paperCopyAvailableOnRequest":false}}} diff --git a/tests/installed_wheel_integration.py b/tests/installed_wheel_integration.py index 641728b..05debc9 100644 --- a/tests/installed_wheel_integration.py +++ b/tests/installed_wheel_integration.py @@ -1,118 +1,79 @@ -"""Installed-wheel cross-component proof using only public SDK interfaces.""" +"""Installed-wheel client proof using canonical JSON and public SDK interfaces.""" from __future__ import annotations import json -from datetime import date, datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta, timezone from pathlib import Path import httpx -from pydantic import ValidationError import dpp_sdk from dpp_sdk import ( - BillOfMaterials, - Characteristics, - Dpp4Fun, - Dpp4FunJsonCodec, - DppCore, - DppValidationError, - Material, - Nameplate, - Organization, - OrganizationRole, - PassportMetadata, - ProductClassification, - from_json, - to_json, - validate_dpp4fun, + DataElementCollection, + DigitalProductPassport, + DppHeader, + DppJsonCodec, + Granularity, + SingleValuedDataElement, ) from dpp_sdk.clients import ( DppMappingClientError, DppRegistryClient, DppRepoClient, + DppRepresentation, RegisterDppRequest, ) - - -def _valid_dpp() -> Dpp4Fun: - manufacturer = Organization( - name="Installed Wheel Furniture", - role=OrganizationRole.MANUFACTURER, - ) - core = DppCore( - passportMetadata=PassportMetadata( - uniqueProductIdentifier="11111111-1111-1111-1111-111111111111", - passportUpdateDates=(date(2024, 1, 1),), - ), - nameplate=Nameplate( - gtinCode="GTIN-INSTALLED-1", - manufacturer=manufacturer, - ), - ) - return Dpp4Fun( - coreDpp=core, - classification=ProductClassification( - sector="Furniture", - category="Office Chair", - ), - characteristics=Characteristics( - productName="Installed Chair", - productType="Office Chair", - ), - billOfMaterials=BillOfMaterials( - materials=(Material(name="Steel", portion=1.0),), - ), - ) +from dpp_sdk.dpp4fun import Dpp4Fun, Dpp4FunJsonCodec, Nameplate def main() -> None: module_path = Path(dpp_sdk.__file__).resolve() assert "site-packages" in str(module_path).lower() - - for export in ( - "Dpp4Fun", - "Dpp4FunJsonCodec", - "DppValidationError", - "from_json", - "to_json", - "validate_dpp4fun", - ): - assert getattr(dpp_sdk, export) is not None - assert issubclass(DppValidationError, Exception) - - original = _valid_dpp() - updated = original.with_updates( - characteristics=original.characteristics.with_updates(productName="Installed Chair 2") + assert "DigitalProductPassport" in dpp_sdk.__all__ + assert "Dpp4FunJsonCodec" in dpp_sdk.__all__ + + header = DppHeader( + digital_product_passport_id="11111111-1111-1111-1111-111111111111", + unique_product_identifier="urn:example:product:wheel-test", + granularity=Granularity.MODEL, + dpp_schema_version="1.0", + dpp_status="active", + last_updated=datetime(2026, 1, 2, tzinfo=UTC), + economic_operator_id="urn:example:operator:wheel-test", ) - assert original.characteristics.productName == "Installed Chair" - assert updated.characteristics.productName == "Installed Chair 2" - - try: - updated.characteristics.with_updates(productName=" ") - except ValidationError: - pass - else: # pragma: no cover - executable proof guard - raise AssertionError("invalid immutable update did not re-run validation") - - validate_dpp4fun(updated) - codec = Dpp4FunJsonCodec() - serialized = codec.to_json(updated) - assert serialized == to_json(updated) - restored = codec.from_json(serialized) - assert restored == updated - assert from_json(serialized) == updated - - repository_requests: list[str] = [] - history_date = datetime( - 2024, - 1, - 2, - 4, - 4, - 5, - tzinfo=timezone(timedelta(hours=1)), + core = DigitalProductPassport( + dpp_header=header, + elements=( + SingleValuedDataElement( + element_id="name", + object_type="SingleValuedDataElement", + value="Installed Wheel Furniture", + ), + DataElementCollection( + element_id="extension", + elements=( + SingleValuedDataElement( + element_id="opaqueValue", object_type="SingleValuedDataElement", value=7 + ), + ), + ), + ), ) + core_codec = DppJsonCodec() + core_json = core_codec.to_expanded_json(core) + assert core_codec.to_expanded_json(core_codec.from_expanded_json(core_json)) == core_json + + dpp4fun = Dpp4Fun.create(header, Nameplate.create(manufacturerName="Wheel Manufacturer")) + dpp4fun_codec = Dpp4FunJsonCodec() + dpp4fun_full = dpp4fun_codec.to_expanded_json(dpp4fun) + dpp4fun_compressed = dpp4fun_codec.to_compressed_json(dpp4fun) + restored_dpp4fun = dpp4fun_codec.from_compressed_json(dpp4fun_compressed) + passport = json.loads(dpp4fun_full) + assert dpp4fun_codec.to_expanded_json(restored_dpp4fun) == dpp4fun_full + serialized = json.dumps(passport, separators=(",", ":")) + repository_requests: list[str] = [] + history_date = datetime(2024, 1, 2, 4, 4, 5, tzinfo=timezone(timedelta(hours=1))) history_path = ( b"/v1/dppsByIdAndDate/DPP%7E1%3F%23?date=2024-01-02T03%3A04%3A05Z&representation=full" ) @@ -123,44 +84,32 @@ def repository_handler(request: httpx.Request) -> httpx.Response: if request.method == "POST": assert raw_path == b"/v1/dpps" assert request.content.decode() == serialized - assert json.loads(request.content)["characteristics"]["productName"] == ( - "Installed Chair 2" - ) return httpx.Response( 201, json={ "statusCode": "SuccessCreated", - "payload": {"dppId": "DPP~1?#"}, + "payload": {"digitalProductPassportId": "DPP~1?#"}, }, ) if raw_path == history_path: assert request.method == "GET" return httpx.Response( - 200, - json={"statusCode": "Success", "payload": json.loads(serialized)}, + 200, json={"statusCode": "Success", "payload": json.loads(serialized)} ) - assert raw_path == b"/v1/dpps/mapping-error?representation=full" - return httpx.Response( - 200, - json={"statusCode": "Success", "payload": None}, - ) + assert raw_path == b"/v1/dpps/mapping-error" + return httpx.Response(200, json={"statusCode": "Success", "payload": None}) with httpx.Client(transport=httpx.MockTransport(repository_handler)) as http_client: - repository = DppRepoClient( - "https://repo.example", - codec=codec, - validator=validate_dpp4fun, - client=http_client, + repository = DppRepoClient("https://repo.example", client=http_client) + created = repository.create_dpp(passport) + assert created.digital_product_passport_id == "DPP~1?#" + history = repository.read_dpp_version_by_id_and_date( + "DPP~1?#", history_date, DppRepresentation.FULL ) - assert repository.create_dpp(updated).dppId == "DPP~1?#" - assert repository.read_dpp_version_by_id_and_date("DPP~1?#", history_date) == updated - try: - repository.read_dpp_by_id("mapping-error") - except DppMappingClientError as exc: - assert isinstance(exc.__cause__, ValueError) - assert "payload" in str(exc) - else: # pragma: no cover - executable proof guard - raise AssertionError("null repository payload escaped mapping translation") + assert history.payload == passport + empty_response = repository.read_dpp_by_id("mapping-error") + assert empty_response.payload is None + assert not http_client.is_closed partial_requests: list[bytes] = [] @@ -168,27 +117,21 @@ def partial_handler(request: httpx.Request) -> httpx.Response: partial_requests.append(request.content) if request.url.raw_path == b"/v1/dpps/partial": return httpx.Response( - 200, - json={"statusCode": "Success", "payload": json.loads(serialized)}, + 200, json={"statusCode": "Success", "payload": json.loads(serialized)} ) assert request.url.raw_path == b"/v1/dpps/partial/elements/%24.productName" return httpx.Response(200, json={"statusCode": "Success", "payload": {"accepted": True}}) with httpx.Client(transport=httpx.MockTransport(partial_handler)) as http_client: - partial_repository = DppRepoClient( - "https://repo.example", - codec=codec, - validator=validate_dpp4fun, - client=http_client, - ) - assert partial_repository.update_dpp_by_id("partial", {"weight": 2.5}) == updated - assert partial_repository.update_data_element("partial", "$.productName", "Grüße") == { - "accepted": True - } + repository = DppRepoClient("https://repo.example", client=http_client) + updated = repository.update_dpp_by_id("partial", {"weight": 2.5}) + assert updated.payload == passport + element = repository.update_data_element("partial", "$.productName", "updated") + assert element.payload == {"accepted": True} for operation, invalid, cause_type in ( - (partial_repository.update_dpp_by_id, float("nan"), ValueError), + (repository.update_dpp_by_id, float("nan"), ValueError), ( - lambda dpp_id, value: partial_repository.update_data_element( + lambda dpp_id, value: repository.update_data_element( dpp_id, "$.productName", value ), object(), @@ -202,7 +145,7 @@ def partial_handler(request: httpx.Request) -> httpx.Response: assert "before request" in str(exc) else: # pragma: no cover - executable proof guard raise AssertionError("invalid partial JSON escaped mapping translation") - assert partial_requests == [b'{"weight": 2.5}', b'"Gr\\u00fc\\u00dfe"'] + assert partial_requests == [b'{"weight":2.5}', b'"updated"'] registry_body = ( '{"uniqueProductIdentifier":"11111111-1111-1111-1111-111111111111",' @@ -219,19 +162,18 @@ def registry_handler(request: httpx.Request) -> httpx.Response: assert request.url.raw_path == b"/v1/registerDPP" assert request.content.decode() == registry_body return httpx.Response( - 200, - json={"statusCode": "Success", "payload": {"registrationId": "REG-1"}}, + 200, json={"statusCode": "Success", "payload": {"registrationId": "REG-1"}} ) registry_request = RegisterDppRequest( - uniqueProductIdentifier="11111111-1111-1111-1111-111111111111", - digitalProductPassportId="DPP~1?#", - uniqueEconomicOperatorIdentifier="OP-1", - dppApiEndpoint="https://repo.example/v1/dpps", + unique_product_identifier="11111111-1111-1111-1111-111111111111", + digital_product_passport_id="DPP~1?#", + unique_economic_operator_identifier="OP-1", + dpp_api_endpoint="https://repo.example/v1/dpps", ) with httpx.Client(transport=httpx.MockTransport(registry_handler)) as http_client: registry = DppRegistryClient("https://registry.example", client=http_client) - assert registry.post_new_dpp_to_registry(registry_request).registrationId == "REG-1" + assert registry.post_new_dpp_to_registry(registry_request).registration_id == "REG-1" try: registry.post_new_dpp_to_registry(None) # type: ignore[arg-type] except DppMappingClientError as exc: @@ -247,6 +189,7 @@ def registry_handler(request: httpx.Request) -> httpx.Response: "installed_module": str(module_path), "repository_requests": repository_requests, "registry_requests": registry_calls, + "domain_roundtrips": ["core", "dpp4fun"], "status": "PASSED", }, sort_keys=True, diff --git a/tests/test_clients.py b/tests/test_clients.py index 00870fd..76e71dc 100644 --- a/tests/test_clients.py +++ b/tests/test_clients.py @@ -1,9 +1,8 @@ -"""HTTP client behavior against a mocked httpx transport.""" +"""Final Java client contract checks using deterministic HTTPX transports.""" from __future__ import annotations -import json -from datetime import UTC, datetime, timedelta, timezone +from datetime import UTC, datetime from typing import Any import httpx @@ -11,1197 +10,404 @@ from dpp_sdk.clients import ( DppApiClientError, - DppClientError, + DppApiResponse, DppHttpClientError, DppMappingClientError, DppNetworkClientError, DppRegistryClient, DppRepoClient, - DppValidationClientError, - RegisterDppRequest, - UpdateDataElementRequest, -) -from dpp_sdk.clients.payloads import ( - CreateDppResponse, - DeleteDppResponse, - DppApiMessage, - DppApiResponse, + DppRepresentation, DppStatusCode, MessageType, - ReadDppIdsRequest, - ReadDppIdsResponse, - RegisterDppResponse, -) -from dpp_sdk.core.errors import DppMappingError -from dpp_sdk.dpp4fun.model import Dpp4Fun -from dpp_sdk.dpp4fun.transport import Dpp4FunJsonCodec, to_json -from dpp_sdk.dpp4fun.validation import validate_dpp4fun - - -def _make_repo(handler, **kwargs) -> DppRepoClient[Dpp4Fun]: # type: ignore[no-untyped-def] - client = httpx.Client(transport=httpx.MockTransport(handler)) - return DppRepoClient( - "http://repo.test/", - codec=Dpp4FunJsonCodec(), - validator=validate_dpp4fun, - client=client, - **kwargs, - ) - - -def test_create_returns_dpp_id(valid_dpp4fun: Dpp4Fun) -> None: - captured: dict[str, object] = {} - - def handler(request: httpx.Request) -> httpx.Response: - captured["method"] = request.method - captured["path"] = request.url.path - captured["body"] = json.loads(request.content) - return httpx.Response( - 201, json={"statusCode": "SuccessCreated", "payload": {"dppId": "DPP-1"}} - ) - - repo = _make_repo(handler) - result = repo.create_dpp(valid_dpp4fun) - assert result.dppId == "DPP-1" - assert captured["method"] == "POST" - assert captured["path"] == "/v1/dpps" - # the create body is the flat transport shape - assert "passportMetadata" in captured["body"] # type: ignore[operator] - - -@pytest.mark.parametrize("contract_id", ["CLIENT-REPO-READ-DPP-BY-ID-001"]) -def test_read_by_id_decodes_via_codec(contract_id: str, valid_dpp4fun: Dpp4Fun) -> None: - flat = json.loads(to_json(valid_dpp4fun)) - - def handler(request: httpx.Request) -> httpx.Response: - assert request.url.raw_path == b"/v1/dpps/DPP-1?representation=full" - return httpx.Response(200, json={"statusCode": "Success", "payload": flat}) - - assert _make_repo(handler).read_dpp_by_id("DPP-1") == valid_dpp4fun - - -@pytest.mark.parametrize("contract_id", ["CLIENT-REPO-READ-DPP-BY-PRODUCT-ID-001"]) -def test_read_by_product_id_encodes_path(contract_id: str) -> None: - def handler(request: httpx.Request) -> httpx.Response: - assert request.url.path == "/v1/dppsByProductId/GTIN 0001" - assert request.url.raw_path == b"/v1/dppsByProductId/GTIN%200001?representation=full" - return httpx.Response(200, json={"statusCode": "Success", "payload": {"x": 1}}) - - # codec.from_json will fail on this dummy payload; we only assert the URL. - with pytest.raises(Exception): # noqa: B017, PT011 - _make_repo(handler).read_dpp_by_product_id("GTIN 0001") - - -@pytest.mark.parametrize("contract_id", ["CLIENT-REPO-READ-DPP-IDS-BY-PRODUCT-IDS-001"]) -def test_read_dpp_ids(contract_id: str) -> None: - def handler(request: httpx.Request) -> httpx.Response: - assert request.url.raw_path == b"/v1/dppsByProductIds" - return httpx.Response( - 200, - json={ - "statusCode": "Success", - "payload": {"dppIdentifiers": ["a", "b"], "nextCursor": "c2"}, - }, - ) - - result = _make_repo(handler).read_dpp_ids_by_product_ids(["p1"], limit=10) - assert result.dppIdentifiers == ["a", "b"] - assert result.nextCursor == "c2" - - -@pytest.mark.parametrize("contract_id", ["CLIENT-REPO-UPDATE-DATA-ELEMENT-001"]) -def test_update_data_element_sends_direct_payload(contract_id: str) -> None: - seen: dict[str, object] = {} - - def handler(request: httpx.Request) -> httpx.Response: - seen["path"] = request.url.path - seen["body"] = json.loads(request.content) - return httpx.Response(200, json={"statusCode": "Success", "payload": 42}) - - result = _make_repo(handler).update_data_element("DPP-1", "$.weight", 42) - assert result == 42 - assert seen["path"] == "/v1/dpps/DPP-1/elements/$.weight" - assert seen["body"] == 42 - - -@pytest.mark.parametrize("contract_id", ["CLIENT-REPO-READ-DATA-ELEMENT-001"]) -def test_read_data_element_returns_the_raw_selected_json_value(contract_id: str) -> None: - def handler(request: httpx.Request) -> httpx.Response: - assert request.method == "GET" - assert request.url.raw_path == b"/v1/dpps/DPP%201/elements/%24.characteristics.weight" - return httpx.Response(200, json={"statusCode": "Success", "payload": 42}) - - assert _make_repo(handler).read_data_element("DPP 1", "$.characteristics.weight") == 42 - - -@pytest.mark.parametrize( - "operation", - [ - pytest.param("read", id="element-read"), - pytest.param("update", id="element-update"), - ], -) -def test_element_null_payload_is_a_causal_mapping_error(operation: str) -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"statusCode": "Success", "payload": None}) - - repo = _make_repo(handler) - with pytest.raises(DppMappingClientError, match="payload") as exc: - if operation == "read": - repo.read_data_element("DPP-1", "$.weight") - else: - repo.update_data_element("DPP-1", "$.weight", 42) - assert isinstance(exc.value.__cause__, ValueError) - assert "payload" in str(exc.value.__cause__) - - -def test_compatibility_element_dto_cannot_change_canonical_direct_body() -> None: - seen: dict[str, object] = {} - - def handler(request: httpx.Request) -> httpx.Response: - seen["body"] = json.loads(request.content) - return httpx.Response(200, json={"statusCode": "Success", "payload": {"accepted": True}}) - - compatibility_dto = UpdateDataElementRequest(payload={"weight": 42}) - _make_repo(handler).update_data_element("DPP-1", "$.weight", compatibility_dto.payload) - assert seen["body"] == {"weight": 42} - - -@pytest.mark.parametrize("contract_id", ["CLIENT-REPO-DELETE-DPP-BY-ID-001"]) -def test_delete_returns_status(contract_id: str) -> None: - def handler(request: httpx.Request) -> httpx.Response: - assert request.method == "DELETE" - assert request.url.raw_path == b"/v1/dpps/DPP-1" - return httpx.Response(200, json={"statusCode": "SuccessNoContent", "messages": []}) - - assert _make_repo(handler).delete_dpp_by_id("DPP-1").statusCode is not None - - -@pytest.mark.parametrize("contract_id", ["CLIENT-REPO-READ-COMPRESSED-DPP-BY-ID-001"]) -def test_read_compressed_dpp_returns_raw_payload_without_codec(contract_id: str) -> None: - def handler(request: httpx.Request) -> httpx.Response: - assert request.url.raw_path == b"/v1/dpps/DPP%201?representation=compressed" - return httpx.Response(200, json={"statusCode": "Success", "payload": {"compressed": True}}) - - assert _make_repo(handler).read_compressed_dpp_by_id("DPP 1") == {"compressed": True} - - -@pytest.mark.parametrize("contract_id", ["CLIENT-REPO-READ-DPP-VERSION-BY-ID-AND-DATE-001"]) -def test_dpp_id_history_uses_versioned_path_and_ordered_query( - contract_id: str, valid_dpp4fun: Dpp4Fun -) -> None: - flat = json.loads(to_json(valid_dpp4fun)) - - def handler(request: httpx.Request) -> httpx.Response: - assert ( - request.url.raw_path == b"/v1/dppsByIdAndDate/DPP%201?date=2026-01-15T12%3A00%3A00Z" - b"&representation=full" - ) - return httpx.Response(200, json={"statusCode": "Success", "payload": flat}) - - assert ( - _make_repo(handler).read_dpp_version_by_id_and_date( - "DPP 1", datetime(2026, 1, 15, 12, tzinfo=UTC) - ) - == valid_dpp4fun - ) - - -@pytest.mark.parametrize("contract_id", ["CLIENT-REPO-READ-DPP-VERSION-BY-PRODUCT-ID-AND-DATE-001"]) -def test_product_id_history_is_retained_unversioned_compatibility( - contract_id: str, valid_dpp4fun: Dpp4Fun -) -> None: - flat = json.loads(to_json(valid_dpp4fun)) - - def handler(request: httpx.Request) -> httpx.Response: - assert ( - request.url.raw_path - == b"/dppsByProductIdAndDate/GTIN%201?date=2026-01-15T12%3A00%3A00Z" - ) - return httpx.Response(200, json={"statusCode": "Success", "payload": flat}) - - assert ( - _make_repo(handler).read_dpp_version_by_product_id_and_date( - "GTIN 1", datetime(2026, 1, 15, 12, tzinfo=UTC) - ) - == valid_dpp4fun - ) - - -@pytest.mark.parametrize( - ("date", "expected_query"), - [ - pytest.param( - datetime(2024, 1, 2, 3, 4, 5, tzinfo=UTC), - b"2024-01-02T03%3A04%3A05Z", - id="utc-aware", - ), - pytest.param( - datetime(2024, 1, 2, 4, 4, 5, tzinfo=timezone(timedelta(hours=1))), - b"2024-01-02T03%3A04%3A05Z", - id="positive-offset", - ), - pytest.param( - datetime(2024, 1, 1, 22, 4, 5, tzinfo=timezone(timedelta(hours=-5))), - b"2024-01-02T03%3A04%3A05Z", - id="negative-offset-crosses-forward-date-boundary", - ), - pytest.param( - datetime(2024, 1, 2, 0, 30, tzinfo=timezone(timedelta(hours=1))), - b"2024-01-01T23%3A30%3A00Z", - id="positive-offset-crosses-backward-date-boundary", - ), - pytest.param( - datetime(2024, 1, 2, 3, 4, 5, 123000, tzinfo=UTC), - b"2024-01-02T03%3A04%3A05.123Z", - id="millisecond-fraction", - ), - pytest.param( - datetime(2024, 1, 2, 3, 4, 5, 123456, tzinfo=UTC), - b"2024-01-02T03%3A04%3A05.123456Z", - id="microsecond-fraction", - ), - ], -) -@pytest.mark.parametrize( - ("route_family", "expected_prefix", "expected_suffix"), - [ - pytest.param( - "dpp-id", - b"/v1/dppsByIdAndDate/DPP%201?date=", - b"&representation=full", - id="dpp-id-history", - ), - pytest.param( - "product-id", - b"/dppsByProductIdAndDate/DPP%201?date=", - b"", - id="product-id-history", - ), - ], + RegisterDppRequest, ) -def test_history_timestamp_wire_is_canonical_utc_z( - route_family: str, - expected_prefix: bytes, - expected_suffix: bytes, - date: datetime, - expected_query: bytes, - valid_dpp4fun: Dpp4Fun, -) -> None: - flat = json.loads(to_json(valid_dpp4fun)) - - def handler(request: httpx.Request) -> httpx.Response: - assert request.url.raw_path == expected_prefix + expected_query + expected_suffix - return httpx.Response(200, json={"statusCode": "Success", "payload": flat}) - - repo = _make_repo(handler) - if route_family == "dpp-id": - result = repo.read_dpp_version_by_id_and_date("DPP 1", date) - else: - result = repo.read_dpp_version_by_product_id_and_date("DPP 1", date) - assert result == valid_dpp4fun +SUCCESS = {"statusCode": "Success", "payload": {"id": "d-1"}, "message": []} -@pytest.mark.parametrize( - ("value", "encoded"), - [ - pytest.param("*", b"*", id="star"), - pytest.param("~", b"%7E", id="tilde"), - pytest.param("a b", b"a%20b", id="space"), - pytest.param("/", b"%2F", id="slash"), - pytest.param("+", b"%2B", id="plus"), - pytest.param("%", b"%25", id="percent"), - pytest.param("?", b"%3F", id="question-mark"), - pytest.param("#", b"%23", id="hash"), - pytest.param("a~b%?#", b"a%7Eb%25%3F%23", id="refreshed-parent-combined-vector"), - pytest.param("Grüße", b"Gr%C3%BC%C3%9Fe", id="unicode-non-ascii"), - pytest.param("a%2Fb", b"a%252Fb", id="already-percent-looking"), - ], -) -@pytest.mark.parametrize( - "route_family", - [ - pytest.param("delete-dpp-by-id", id="delete-dpp-by-id"), - pytest.param("read-compressed-dpp-by-id", id="read-compressed-dpp-by-id"), - pytest.param("read-data-element", id="read-data-element"), - pytest.param("read-dpp-by-id", id="read-dpp-by-id"), - pytest.param("read-dpp-by-product-id", id="read-dpp-by-product-id"), - pytest.param("read-dpp-version-by-id-and-date", id="read-dpp-version-by-id-and-date"), - pytest.param( - "read-dpp-version-by-product-id-and-date", - id="read-dpp-version-by-product-id-and-date", - ), - pytest.param("update-data-element", id="update-data-element"), - pytest.param("update-dpp-by-id", id="update-dpp-by-id"), - ], -) -def test_dynamic_path_segment_wire_is_exact_for_every_route_family( - route_family: str, - value: str, - encoded: bytes, - valid_dpp4fun: Dpp4Fun, -) -> None: - flat = json.loads(to_json(valid_dpp4fun)) - if route_family == "read-dpp-by-product-id": - expected_path = b"/v1/dppsByProductId/" + encoded + b"?representation=full" - elif route_family == "read-dpp-version-by-id-and-date": - expected_path = ( - b"/v1/dppsByIdAndDate/" - + encoded - + b"?date=2024-01-02T03%3A04%3A05Z&representation=full" - ) - elif route_family == "read-dpp-version-by-product-id-and-date": - expected_path = b"/dppsByProductIdAndDate/" + encoded + b"?date=2024-01-02T03%3A04%3A05Z" - elif route_family in {"read-data-element", "update-data-element"}: - expected_path = b"/v1/dpps/" + encoded + b"/elements/" + encoded - elif route_family == "read-dpp-by-id": - expected_path = b"/v1/dpps/" + encoded + b"?representation=full" - elif route_family == "read-compressed-dpp-by-id": - expected_path = b"/v1/dpps/" + encoded + b"?representation=compressed" - else: - expected_path = b"/v1/dpps/" + encoded +def transport_for(status: int = 200, body: Any = SUCCESS, seen: list[httpx.Request] | None = None): def handler(request: httpx.Request) -> httpx.Response: - assert request.url.raw_path == expected_path - status = "SuccessNoContent" if route_family == "delete-dpp-by-id" else "Success" - payload: Any = 42 if route_family in {"read-data-element", "update-data-element"} else flat - return httpx.Response(200, json={"statusCode": status, "payload": payload}) + if seen is not None: + seen.append(request) + content = body if isinstance(body, (str, bytes)) else __import__("json").dumps(body) + return httpx.Response(status, content=content, request=request) - repo = _make_repo(handler) - if route_family == "delete-dpp-by-id": - repo.delete_dpp_by_id(value) - elif route_family == "read-compressed-dpp-by-id": - repo.read_compressed_dpp_by_id(value) - elif route_family == "read-data-element": - repo.read_data_element(value, value) - elif route_family == "read-dpp-by-id": - repo.read_dpp_by_id(value) - elif route_family == "read-dpp-by-product-id": - repo.read_dpp_by_product_id(value) - elif route_family == "read-dpp-version-by-id-and-date": - repo.read_dpp_version_by_id_and_date(value, datetime(2024, 1, 2, 3, 4, 5, tzinfo=UTC)) - elif route_family == "read-dpp-version-by-product-id-and-date": - repo.read_dpp_version_by_product_id_and_date( - value, datetime(2024, 1, 2, 3, 4, 5, tzinfo=UTC) - ) - elif route_family == "update-data-element": - repo.update_data_element(value, value, {"updated": True}) - else: - repo.update_dpp_by_id(value, {"updated": True}) + return httpx.MockTransport(handler) -@pytest.mark.parametrize("contract_id", ["CLIENT-REPO-UPDATE-DPP-BY-ID-001"]) -def test_partial_patch_decodes_response_without_whole_model_validation( - contract_id: str, - valid_dpp4fun: Dpp4Fun, -) -> None: - flat = json.loads(to_json(valid_dpp4fun)) - validator_calls = 0 - - def handler(request: httpx.Request) -> httpx.Response: - assert request.url.raw_path == b"/v1/dpps/DPP%201" - assert json.loads(request.content) == {"characteristics": {"productName": "updated"}} - return httpx.Response(200, json={"statusCode": "Success", "payload": flat}) +def client_for( + klass: type[Any], + body: Any = SUCCESS, + status: int = 200, + seen: list[httpx.Request] | None = None, +): + client = httpx.Client(transport=transport_for(status, body, seen)) + return klass("https://api.example/root/", client=client), client - def validator(dpp: Dpp4Fun) -> None: - nonlocal validator_calls - validator_calls += 1 - repo = DppRepoClient( - "http://repo.test", - codec=Dpp4FunJsonCodec(), - validator=validator, - client=httpx.Client(transport=httpx.MockTransport(handler)), - ) - assert ( - repo.update_dpp_by_id("DPP 1", {"characteristics": {"productName": "updated"}}) - == valid_dpp4fun +def test_public_payload_contracts_and_canonical_message_cardinality() -> None: + response = DppApiResponse.model_validate( + {"statusCode": "Success", "payload": {}, "message": [{"messageType": "Info", "text": "ok"}]} ) - assert validator_calls == 0 - - -@pytest.mark.parametrize( - ("operation", "payload", "cause_type"), - [ - pytest.param("full", float("nan"), ValueError, id="full-top-level-nan"), - pytest.param("element", float("nan"), ValueError, id="element-top-level-nan"), - pytest.param("full", {"nested": [float("nan")]}, ValueError, id="full-nested-nan"), - pytest.param("element", {"nested": [float("nan")]}, ValueError, id="element-nested-nan"), - pytest.param("full", float("inf"), ValueError, id="full-positive-infinity"), - pytest.param("element", float("inf"), ValueError, id="element-positive-infinity"), - pytest.param("full", float("-inf"), ValueError, id="full-negative-infinity"), - pytest.param("element", float("-inf"), ValueError, id="element-negative-infinity"), - pytest.param("full", object(), TypeError, id="full-object"), - pytest.param("element", {"bad": {1, 2}}, TypeError, id="element-set"), - ], -) -def test_partial_updates_reject_unmappable_json_before_transport( - operation: str, payload: Any, cause_type: type[Exception] -) -> None: - calls = 0 - - def handler(request: httpx.Request) -> httpx.Response: - nonlocal calls - calls += 1 - pytest.fail("invalid partial JSON must not reach the transport") - - repo = _make_repo(handler) - with pytest.raises( - DppMappingClientError, - match=f"update_{'dpp_by_id' if operation == 'full' else 'data_element'}", - ) as exc: - if operation == "full": - repo.update_dpp_by_id("DPP-1", payload) - else: - repo.update_data_element("DPP-1", "$.weight", payload) - - assert isinstance(exc.value.__cause__, cause_type) - assert calls == 0 + assert response.message is not None + assert response.message[0].message_type is MessageType.Info + assert DppStatusCode.SuccessCreated.is_success + assert not DppStatusCode.ServerInternalError.is_success + assert DppApiResponse.model_validate({"statusCode": "Success", "messages": []}).message is None + with pytest.raises(ValueError): + DppStatusCode("ClientErrorNotAuthorized") + with pytest.raises(ValueError): + DppStatusCode("ClientErrorForbidden") -@pytest.mark.parametrize("operation", ["full", "element"]) @pytest.mark.parametrize( - "payload", + "message", [ - pytest.param(7, id="integer"), - pytest.param(2.5, id="float"), - pytest.param("Möbel 🪑", id="unicode"), - pytest.param(None, id="null"), + {}, + {"messageType": None, "text": "detail"}, + {"messageType": "Info"}, + {"messageType": "Info", "text": None}, ], ) -def test_partial_updates_preserve_valid_json_wire_bodies( - operation: str, payload: Any, valid_dpp4fun: Dpp4Fun -) -> None: - captured: list[bytes] = [] - flat = json.loads(to_json(valid_dpp4fun)) - - def handler(request: httpx.Request) -> httpx.Response: - captured.append(request.content) - response_payload: Any = flat if operation == "full" else {"accepted": True} - return httpx.Response(200, json={"statusCode": "Success", "payload": response_payload}) - - repo = _make_repo(handler) - if operation == "full": - assert repo.update_dpp_by_id("DPP-1", payload) == valid_dpp4fun - else: - assert repo.update_data_element("DPP-1", "$.weight", payload) == {"accepted": True} - - assert captured == [json.dumps(payload).encode()] - - -@pytest.mark.parametrize("payload", [None, "text", 1, True, ["a"], {"x": 1}]) -def test_element_patch_preserves_every_json_value_and_bypasses_validator(payload: Any) -> None: - calls = 0 - - def handler(request: httpx.Request) -> httpx.Response: - assert request.url.raw_path == b"/v1/dpps/DPP%201/elements/%24%5B%27a%2Fb%27%5D" - assert json.loads(request.content) == payload - return httpx.Response(200, json={"statusCode": "Success", "payload": {"accepted": True}}) +def test_api_message_requires_table_13_fields(message: dict[str, Any]) -> None: + with pytest.raises(ValueError): + DppApiResponse.model_validate({"statusCode": "Success", "message": [message]}) - def validator(dpp: Dpp4Fun) -> None: - nonlocal calls - calls += 1 - repo = DppRepoClient( - "http://repo.test", - codec=Dpp4FunJsonCodec(), - validator=validator, - client=httpx.Client(transport=httpx.MockTransport(handler)), +def test_api_message_allows_blank_text_and_optional_response_messages() -> None: + for envelope in ( + {"statusCode": "Success"}, + {"statusCode": "Success", "message": None}, + {"statusCode": "Success", "message": []}, + ): + DppApiResponse.model_validate(envelope) + response = DppApiResponse.model_validate( + {"statusCode": "Success", "message": [{"messageType": "Info", "text": ""}]} ) - assert repo.update_data_element("DPP 1", "$['a/b']", payload) == {"accepted": True} - assert calls == 0 - + assert response.message is not None and response.message[0].text == "" -def test_repository_identifier_and_element_preconditions_fail_before_network() -> None: - def handler(request: httpx.Request) -> httpx.Response: - pytest.fail("local precondition must prevent network I/O") - repo = _make_repo(handler) - with pytest.raises(ValueError): - repo.read_dpp_by_id(" ") - with pytest.raises(ValueError): - repo.update_data_element("D", " ", None) +def test_api_response_rejects_null_message_item() -> None: with pytest.raises(ValueError): - repo.read_dpp_version_by_id_and_date("D", datetime(2026, 1, 15)) + DppApiResponse.model_validate({"statusCode": "Success", "message": [None]}) -def test_api_error_status_raises_with_messages() -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response( - 200, - json={ - "statusCode": "ClientErrorResourceNotFound", - "messages": [{"messageType": "Error", "text": "not found"}], - }, - ) - - with pytest.raises(DppApiClientError) as exc: - _make_repo(handler).read_dpp_by_id("missing") - assert exc.value.messages is not None - assert exc.value.messages[0].text == "not found" - - -def test_http_error_status_raises() -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(500, text="boom") - - with pytest.raises(DppHttpClientError) as exc: - _make_repo(handler).read_dpp_by_id("x") - assert exc.value.status_code == 500 - - -def test_status_code_alias_is_accepted() -> None: - def handler(request: httpx.Request) -> httpx.Response: - # The Java @JsonCreator alias must map to ClientForbidden. - return httpx.Response(200, json={"statusCode": "ClientErrorForbidden", "messages": []}) - - with pytest.raises(DppApiClientError) as exc: - _make_repo(handler).read_dpp_by_id("x") - assert exc.value.status_code is not None - assert exc.value.status_code.value == "ClientForbidden" - - -def test_shared_payloads_emit_canonical_status_and_timestamp() -> None: - response = DppApiResponse.model_validate( - { - "statusCode": "ServerNotImplemented", - "messages": [ - { - "messageType": "Warning", - "correlationId": "c-1", - "timestamp": "2026-01-15T12:00:00+00:00", - } - ], - "unknown": "ignored", - } +def test_payload_python_names_are_snake_case_and_wire_aliases_remain_canonical() -> None: + request = RegisterDppRequest( + unique_product_identifier="p1", + digital_product_passport_id="d1", + unique_economic_operator_identifier="o1", + dpp_api_endpoint="https://repo.example", ) - assert response.statusCode is DppStatusCode.ServerNotImplemented - assert response.model_dump(mode="json")["statusCode"] == "ServerNotImplemented" - assert response.messages == [ - DppApiMessage( - messageType=MessageType.Warning, - correlationId="c-1", - timestamp=response.messages[0].timestamp if response.messages else None, - ) - ] - - -@pytest.mark.parametrize( - "response_payload", - [ - pytest.param({"registrationId": "REG-1"}, id="canonical-response"), - pytest.param({"registryIdentifier": "REG-1"}, id="legacy-response-input"), - ], -) -def test_registry_register_uses_canonical_wire_contract( - response_payload: dict[str, str], -) -> None: - def handler(request: httpx.Request) -> httpx.Response: - assert request.method == "POST" - assert request.url.raw_path == b"/v1/registerDPP" - assert request.headers["content-type"] == "application/json" - assert json.loads(request.content) == { - "uniqueProductIdentifier": "GTIN-0001", - "digitalProductPassportId": "DPP-1", - "uniqueEconomicOperatorIdentifier": None, - "dppApiEndpoint": None, - } - return httpx.Response( - 201, - json={"statusCode": "SuccessCreated", "payload": response_payload}, - ) - client = httpx.Client(transport=httpx.MockTransport(handler)) - registry = DppRegistryClient("http://reg.test", client=client) - response = registry.post_new_dpp_to_registry( - RegisterDppRequest.model_validate( - {"productIdentifier": "GTIN-0001", "dppIdentifier": "DPP-1"} - ) + assert request.model_dump(mode="json", by_alias=True, exclude_none=False) == { + "uniqueProductIdentifier": "p1", + "digitalProductPassportId": "d1", + "uniqueEconomicOperatorIdentifier": "o1", + "dppApiEndpoint": "https://repo.example", + } + assert not hasattr(request, "uniqueProductIdentifier") + + +@pytest.mark.parametrize("client_type", [DppRepoClient, DppRegistryClient]) +def test_client_rejects_malformed_base_uri(client_type) -> None: + for base_url in ( + "https://example.com/%zz", + "example.com/api", + "ftp://example.com/api", + "https:///missing-host", + "https://example.com:bad", + "https://example.com:99999", + "https://example.com?tenant=one", + "https://example.com#fragment", + "https://example.com?", + "https://example.com#", + ): + with pytest.raises(ValueError): + client_type(base_url) + + +@pytest.mark.parametrize("client_type", [DppRepoClient, DppRegistryClient]) +def test_client_accepts_absolute_http_localhost_base_uri(client_type) -> None: + client = client_type("http://localhost:8080/api") + client.close() + + +def test_create_response_ignores_legacy_identifier_alias() -> None: + client, caller_client = client_for( + DppRepoClient, {"statusCode": "Success", "payload": {"dppId": "old"}} ) - assert response.registrationId == "REG-1" - assert response.model_dump() == {"registrationId": "REG-1"} + try: + result = client.create_dpp({"digitalProductPassportId": "canonical"}) + assert result.digital_product_passport_id is None + finally: + caller_client.close() @pytest.mark.parametrize( - "registry_request", + ("operation", "method", "url", "body", "representation"), [ - pytest.param(None, id="null-request"), - pytest.param( - RegisterDppRequest.model_construct(uniqueProductIdentifier=object()), - id="structurally-invalid-request", + ("create", "POST", "https://api.example/root/v1/dpps", {"id": "d-1"}, None), + ( + "create_full", + "POST", + "https://api.example/root/v1/dpps?representation=full", + {"id": "d-1"}, + None, + ), + ( + "read_id", + "GET", + "https://api.example/root/v1/dpps/d%2F1?representation=full", + None, + "FULL", + ), + ("read_product", "GET", "https://api.example/root/v1/dppsByProductId/p%2F1", None, None), + ( + "history", + "GET", + "https://api.example/root/v1/dppsByIdAndDate/d%2F1?date=2026-05-11T09%3A30%3A15Z&representation=full", + None, + "FULL", + ), + ( + "ids", + "POST", + "https://api.example/root/v1/dppsByProductIds", + {"productIdentifiers": ["p1"], "limit": 5, "cursor": "c"}, + None, + ), + ( + "update", + "PATCH", + "https://api.example/root/v1/dpps/d%2F1?representation=full", + {"name": "n"}, + "FULL", + ), + ("delete", "DELETE", "https://api.example/root/v1/dpps/d%2F1", None, None), + ( + "element_read", + "GET", + "https://api.example/root/v1/dpps/d%2F1/elements/%24%5B%27name%27%5D?representation=full", + None, + "FULL", + ), + ( + "element_update", + "PATCH", + "https://api.example/root/v1/dpps/d%2F1/elements/%24%5B%27name%27%5D", + "value", + None, ), ], ) -def test_registry_invalid_request_is_a_causal_mapping_error_before_network( - registry_request: RegisterDppRequest | None, +def test_repository_operation_contracts( + operation: str, method: str, url: str, body: Any, representation: str | None ) -> None: - calls = 0 - - def handler(http_request: httpx.Request) -> httpx.Response: - nonlocal calls - calls += 1 - return httpx.Response( - 200, json={"statusCode": "Success", "payload": {"registrationId": "R"}} - ) - - registry = DppRegistryClient( - "http://reg.test", - client=httpx.Client(transport=httpx.MockTransport(handler)), + seen: list[httpx.Request] = [] + payload = ( + {"dppIdentifiers": ["d-1"]} + if operation == "ids" + else {"digitalProductPassportId": "d-1"} + if operation.startswith("create") + else {"id": "d-1"} ) - with pytest.raises(DppMappingClientError, match="request") as exc: - registry.post_new_dpp_to_registry(registry_request) # type: ignore[arg-type] - assert exc.value.__cause__ is not None - assert str(exc.value.__cause__) - assert not isinstance(exc.value.__cause__, AttributeError) - assert calls == 0 - - -@pytest.mark.parametrize( - ("contract_id", "response", "expected_error"), - [ - pytest.param( - "CLIENT-REGISTRY-POST-NEW-DPP-TO-REGISTRY-001-http", - httpx.Response(500, text="boom"), - DppHttpClientError, - id="CLIENT-REGISTRY-POST-NEW-DPP-TO-REGISTRY-001-http", - ), - pytest.param( - "CLIENT-REGISTRY-POST-NEW-DPP-TO-REGISTRY-001-malformed-envelope", - httpx.Response(200, text="not-json"), - DppMappingClientError, - id="CLIENT-REGISTRY-POST-NEW-DPP-TO-REGISTRY-001-malformed-envelope", - ), - pytest.param( - "CLIENT-REGISTRY-POST-NEW-DPP-TO-REGISTRY-001-api-error", - httpx.Response(200, json={"statusCode": "ClientErrorBadRequest", "messages": []}), - DppApiClientError, - id="CLIENT-REGISTRY-POST-NEW-DPP-TO-REGISTRY-001-api-error", - ), - pytest.param( - "CLIENT-REGISTRY-POST-NEW-DPP-TO-REGISTRY-001-missing-payload", - httpx.Response(200, json={"statusCode": "Success"}), - DppMappingClientError, - id="CLIENT-REGISTRY-POST-NEW-DPP-TO-REGISTRY-001-missing-payload", - ), - pytest.param( - "CLIENT-REGISTRY-POST-NEW-DPP-TO-REGISTRY-001-blank-registration-id", - httpx.Response(200, json={"statusCode": "Success", "payload": {"registrationId": " "}}), - DppMappingClientError, - id="CLIENT-REGISTRY-POST-NEW-DPP-TO-REGISTRY-001-blank-registration-id", - ), - pytest.param( - "CLIENT-REGISTRY-POST-NEW-DPP-TO-REGISTRY-001-unmappable-registration-id", - httpx.Response(200, json={"statusCode": "Success", "payload": {"registrationId": []}}), - DppMappingClientError, - id="CLIENT-REGISTRY-POST-NEW-DPP-TO-REGISTRY-001-unmappable-registration-id", - ), - ], -) -def test_registry_register_preserves_shared_error_categories( - contract_id: str, response: httpx.Response, expected_error: type[Exception] -) -> None: - registry = DppRegistryClient( - "http://reg.test", - client=httpx.Client(transport=httpx.MockTransport(lambda request: response)), + client, raw = client_for( + DppRepoClient, {"statusCode": "Success", "payload": payload, "message": []}, seen=seen ) - with pytest.raises(expected_error): - registry.post_new_dpp_to_registry(RegisterDppRequest()) + rep = DppRepresentation[representation] if representation else DppRepresentation.COMPRESSED + date = datetime(2026, 5, 11, 9, 30, 15, tzinfo=UTC) + calls = { + "create": lambda: client.create_dpp(body, rep), + "create_full": lambda: client.create_dpp(body, DppRepresentation.FULL), + "read_id": lambda: client.read_dpp_by_id("d/1", rep), + "read_product": lambda: client.read_dpp_by_product_id("p/1"), + "history": lambda: client.read_dpp_version_by_id_and_date("d/1", date, rep), + "ids": lambda: client.read_dpp_ids_by_product_ids(["p1"], 5, "c"), + "update": lambda: client.update_dpp_by_id("d/1", body, rep), + "delete": lambda: client.delete_dpp_by_id("d/1"), + "element_read": lambda: client.read_data_element("d/1", "$['name']", rep), + "element_update": lambda: client.update_data_element("d/1", "$['name']", body), + } + result = calls[operation]() + request = seen[0] + assert request.method == method + assert str(request.url) == url + if body is not None: + assert request.headers["content-type"] == "application/json" + assert request.read() == __import__("json").dumps(body, separators=(",", ":")).encode() + assert result is not None + raw.close() -@pytest.mark.parametrize( - "error", - [ - pytest.param(httpx.ReadTimeout("timeout"), id="timeout"), - pytest.param(httpx.ConnectError("transport"), id="transport"), - ], -) -def test_registry_register_translates_network_errors_with_cause(error: Exception) -> None: - registry = DppRegistryClient( - "http://reg.test", - client=httpx.Client( - transport=httpx.MockTransport(lambda request: (_ for _ in ()).throw(error)) - ), +def test_registry_operation_uses_canonical_request_and_response_fields() -> None: + seen: list[httpx.Request] = [] + registry, raw = client_for( + DppRegistryClient, + {"statusCode": "SuccessCreated", "payload": {"registrationId": "r1"}, "message": []}, + seen=seen, ) - with pytest.raises(DppNetworkClientError) as exc: - registry.post_new_dpp_to_registry(RegisterDppRequest()) - assert exc.value.__cause__ is error - - -def test_registry_payload_canonical_keys_and_legacy_input_aliases() -> None: - request = RegisterDppRequest.model_validate( - {"productIdentifier": "P", "dppIdentifier": "D", "operatorIdentifier": "O", "repoUrl": "U"} + result = registry.post_new_dpp_to_registry( + RegisterDppRequest( + unique_product_identifier="p1", + digital_product_passport_id="d1", + unique_economic_operator_identifier="o1", + dpp_api_endpoint="https://repo.example", + ) ) - assert request.model_dump() == { - "uniqueProductIdentifier": "P", - "digitalProductPassportId": "D", - "uniqueEconomicOperatorIdentifier": "O", - "dppApiEndpoint": "U", - } - assert RegisterDppResponse.model_validate({"registryIdentifier": "R"}).model_dump() == { - "registrationId": "R" + request = seen[0] + assert request.method == "POST" + assert str(request.url) == "https://api.example/root/v1/registerDPP" + assert __import__("json").loads(request.read()) == { + "uniqueProductIdentifier": "p1", + "digitalProductPassportId": "d1", + "uniqueEconomicOperatorIdentifier": "o1", + "dppApiEndpoint": "https://repo.example", } + assert result.registration_id == "r1" + raw.close() -@pytest.mark.parametrize( - ("contract_id", "status"), - [ - pytest.param( - "PAYLOAD-SHARED-DPP-STATUS-CODE-FIELD-CLIENT-ERROR-BAD-REQUEST", - DppStatusCode.ClientErrorBadRequest, - id="PAYLOAD-SHARED-DPP-STATUS-CODE-FIELD-CLIENT-ERROR-BAD-REQUEST", - ), - pytest.param( - "PAYLOAD-SHARED-DPP-STATUS-CODE-FIELD-CLIENT-METHOD-NOT-ALLOWED", - DppStatusCode.ClientMethodNotAllowed, - id="PAYLOAD-SHARED-DPP-STATUS-CODE-FIELD-CLIENT-METHOD-NOT-ALLOWED", - ), - pytest.param( - "PAYLOAD-SHARED-DPP-STATUS-CODE-FIELD-CLIENT-NOT-AUTHORIZED", - DppStatusCode.ClientNotAuthorized, - id="PAYLOAD-SHARED-DPP-STATUS-CODE-FIELD-CLIENT-NOT-AUTHORIZED", - ), - pytest.param( - "PAYLOAD-SHARED-DPP-STATUS-CODE-FIELD-CLIENT-RESOURCE-CONFLICT", - DppStatusCode.ClientResourceConflict, - id="PAYLOAD-SHARED-DPP-STATUS-CODE-FIELD-CLIENT-RESOURCE-CONFLICT", - ), - pytest.param( - "PAYLOAD-SHARED-DPP-STATUS-CODE-FIELD-SERVER-ERROR-BAD-GATEWAY", - DppStatusCode.ServerErrorBadGateway, - id="PAYLOAD-SHARED-DPP-STATUS-CODE-FIELD-SERVER-ERROR-BAD-GATEWAY", - ), - pytest.param( - "PAYLOAD-SHARED-DPP-STATUS-CODE-FIELD-SERVER-INTERNAL-ERROR", - DppStatusCode.ServerInternalError, - id="PAYLOAD-SHARED-DPP-STATUS-CODE-FIELD-SERVER-INTERNAL-ERROR", - ), - pytest.param( - "PAYLOAD-SHARED-DPP-STATUS-CODE-FIELD-SERVER-NOT-IMPLEMENTED", - DppStatusCode.ServerNotImplemented, - id="PAYLOAD-SHARED-DPP-STATUS-CODE-FIELD-SERVER-NOT-IMPLEMENTED", - ), - pytest.param( - "PAYLOAD-SHARED-DPP-STATUS-CODE-FIELD-SUCCESS-ACCEPTED", - DppStatusCode.SuccessAccepted, - id="PAYLOAD-SHARED-DPP-STATUS-CODE-FIELD-SUCCESS-ACCEPTED", - ), - ], -) -def test_status_values_round_trip_with_exact_wire_value( - contract_id: str, status: DppStatusCode -) -> None: - assert DppStatusCode(status.value) is status - assert DppApiResponse(statusCode=status).model_dump(mode="json")["statusCode"] == status.value - - -@pytest.mark.parametrize( - "contract_id", - [ - pytest.param("PAYLOAD-SHARED-DPP-STATUS-CODE", id="PAYLOAD-SHARED-DPP-STATUS-CODE"), - pytest.param("PAYLOAD-SHARED-DPP-API-RESPONSE", id="PAYLOAD-SHARED-DPP-API-RESPONSE"), - pytest.param("PAYLOAD-SHARED-DPP-API-MESSAGE", id="PAYLOAD-SHARED-DPP-API-MESSAGE"), - pytest.param( - "PAYLOAD-SHARED-DPP-API-MESSAGE-FIELD-CORRELATION-ID", - id="PAYLOAD-SHARED-DPP-API-MESSAGE-FIELD-CORRELATION-ID", - ), - pytest.param( - "PAYLOAD-SHARED-DPP-API-MESSAGE-FIELD-TIMESTAMP", - id="PAYLOAD-SHARED-DPP-API-MESSAGE-FIELD-TIMESTAMP", - ), - pytest.param("PAYLOAD-SHARED-MESSAGE-TYPE", id="PAYLOAD-SHARED-MESSAGE-TYPE"), - pytest.param( - "PAYLOAD-SHARED-MESSAGE-TYPE-FIELD-WARNING", - id="PAYLOAD-SHARED-MESSAGE-TYPE-FIELD-WARNING", - ), - ], -) -def test_shared_payload_defaults_timestamp_and_unknown_fields(contract_id: str) -> None: - timestamp = datetime(2026, 1, 15, 12, tzinfo=UTC) - response = DppApiResponse.model_validate( +def test_204_and_operation_specific_payloads_keep_envelope_metadata() -> None: + message = [{"messageType": "Warning", "text": "accepted without body", "code": "C1"}] + repo, raw = client_for( + DppRepoClient, { - "statusCode": "SuccessAccepted", - "payload": None, - "messages": [ - { - "messageType": "Warning", - "correlationId": "c-1", - "timestamp": timestamp.isoformat(), - } - ], - "unknown": "ignored", - } + "statusCode": "SuccessCreated", + "payload": {"digitalProductPassportId": "d1"}, + "message": message, + }, + status=201, ) - assert response.payload is None - assert response.messages is not None - assert response.messages[0].timestamp == timestamp - assert response.model_dump(mode="json")["messages"][0]["timestamp"] == "2026-01-15T12:00:00Z" - + created = repo.create_dpp({"anything": True}) + assert created.digital_product_passport_id == "d1" + assert created.status_code is DppStatusCode.SuccessCreated + assert created.message is not None and created.message[0].code == "C1" + raw.close() -@pytest.mark.parametrize( - "contract_id", - [ - pytest.param("PAYLOAD-REPO-CREATE-DPP-RESPONSE", id="PAYLOAD-REPO-CREATE-DPP-RESPONSE"), - pytest.param("PAYLOAD-REPO-DELETE-DPP-RESPONSE", id="PAYLOAD-REPO-DELETE-DPP-RESPONSE"), - pytest.param("PAYLOAD-REPO-READ-DPP-IDS-REQUEST", id="PAYLOAD-REPO-READ-DPP-IDS-REQUEST"), - pytest.param( - "PAYLOAD-REPO-READ-DPP-IDS-REQUEST-FIELD-PRODUCT-IDENTIFIERS", - id="PAYLOAD-REPO-READ-DPP-IDS-REQUEST-FIELD-PRODUCT-IDENTIFIERS", - ), - pytest.param("PAYLOAD-REPO-READ-DPP-IDS-RESPONSE", id="PAYLOAD-REPO-READ-DPP-IDS-RESPONSE"), - ], -) -def test_repository_dtos_preserve_nulls_defaults_and_unknown_field_policy(contract_id: str) -> None: - assert CreateDppResponse.model_validate({"dppId": "D", "unknown": 1}).dppId == "D" - assert DeleteDppResponse().model_dump() == {"statusCode": None, "messages": None} - assert ReadDppIdsRequest(productIdentifiers=["P"], limit=None, cursor=None).model_dump() == { - "productIdentifiers": ["P"], - "limit": None, - "cursor": None, - } - assert ReadDppIdsResponse.model_validate( - {"dppIdentifiers": ["D"], "unknown": 1} - ).model_dump() == { - "dppIdentifiers": ["D"], - "nextCursor": None, - } - - -@pytest.mark.parametrize( - ("contract_id", "raw"), - [ - pytest.param( - "PAYLOAD-REGISTRY-REGISTER-DPP-REQUEST", - { - "uniqueProductIdentifier": "P", - "digitalProductPassportId": "D", - "uniqueEconomicOperatorIdentifier": "O", - "dppApiEndpoint": "U", - }, - id="PAYLOAD-REGISTRY-REGISTER-DPP-REQUEST", - ), - pytest.param( - "PAYLOAD-REGISTRY-REGISTER-DPP-REQUEST-FIELD-DIGITAL-PRODUCT-PASSPORT-ID", - {"dppIdentifier": "D"}, - id="PAYLOAD-REGISTRY-REGISTER-DPP-REQUEST-FIELD-DIGITAL-PRODUCT-PASSPORT-ID", - ), - pytest.param( - "PAYLOAD-REGISTRY-REGISTER-DPP-REQUEST-FIELD-DPP-API-ENDPOINT", - {"repoUrl": "U"}, - id="PAYLOAD-REGISTRY-REGISTER-DPP-REQUEST-FIELD-DPP-API-ENDPOINT", - ), - pytest.param( - "PAYLOAD-REGISTRY-REGISTER-DPP-REQUEST-FIELD-UNIQUE-ECONOMIC-OPERATOR-IDENTIFIER", - {"operatorIdentifier": "O"}, - id="PAYLOAD-REGISTRY-REGISTER-DPP-REQUEST-FIELD-UNIQUE-ECONOMIC-OPERATOR-IDENTIFIER", - ), - pytest.param( - "PAYLOAD-REGISTRY-REGISTER-DPP-REQUEST-FIELD-UNIQUE-PRODUCT-IDENTIFIER", - {"productIdentifier": "P"}, - id="PAYLOAD-REGISTRY-REGISTER-DPP-REQUEST-FIELD-UNIQUE-PRODUCT-IDENTIFIER", - ), - ], -) -def test_registry_request_aliases_are_input_only_and_canonical_output( - contract_id: str, raw: dict[str, str] -) -> None: - request = RegisterDppRequest.model_validate(raw) - dumped = request.model_dump() - assert set(dumped) == { - "uniqueProductIdentifier", - "digitalProductPassportId", - "uniqueEconomicOperatorIdentifier", - "dppApiEndpoint", - } - assert not {"productIdentifier", "dppIdentifier", "operatorIdentifier", "repoUrl"} & set(dumped) + repo, raw = client_for(DppRepoClient, "", status=204) + deleted = repo.delete_dpp_by_id("d1") + assert deleted.status_code is DppStatusCode.SuccessNoContent + assert deleted.message is None + raw.close() @pytest.mark.parametrize( - "contract_id", + "body", [ - pytest.param( - "PAYLOAD-REGISTRY-REGISTER-DPP-RESPONSE", id="PAYLOAD-REGISTRY-REGISTER-DPP-RESPONSE" - ), - pytest.param( - "PAYLOAD-REGISTRY-REGISTER-DPP-RESPONSE-FIELD-REGISTRATION-ID", - id="PAYLOAD-REGISTRY-REGISTER-DPP-RESPONSE-FIELD-REGISTRATION-ID", - ), + {"statusCode": "Success", "payload": "not-an-object", "message": []}, + {"statusCode": "Success", "payload": {}, "message": []}, ], ) -def test_registry_response_uses_canonical_registration_id(contract_id: str) -> None: - response = RegisterDppResponse.model_validate({"registryIdentifier": "R"}) - assert response.registrationId == "R" - assert response.model_dump() == {"registrationId": "R"} - - -class _EncodingFailureCodec: - def to_json(self, dpp: Any) -> str: - raise ValueError("encode") - - def from_json(self, raw: str) -> Any: - return raw - - -class _DecodingFailureCodec: - def to_json(self, dpp: Any) -> str: - return "{}" - - def from_json(self, raw: str) -> Any: - raise ValueError("decode") - - -class _NullDecodingCodec: - def to_json(self, dpp: Any) -> str: - return "{}" - - def from_json(self, raw: str) -> Any: - return None - - -class _NullEncodingCodec: - def to_json(self, dpp: Any) -> Any: - return None - - def from_json(self, raw: str) -> Any: - return raw - - -def _make_generic_repo(handler: Any, codec: Any, validator: Any) -> DppRepoClient[Any]: - return DppRepoClient( - "http://repo.test", - codec=codec, - validator=validator, - client=httpx.Client(transport=httpx.MockTransport(handler)), - ) - - -def test_validator_failure_precedes_network_and_preserves_sdk_error() -> None: - sent = False - original = DppValidationClientError("already translated") - - def handler(request: httpx.Request) -> httpx.Response: - nonlocal sent - sent = True - return httpx.Response(200) - - def validator(value: Any) -> None: - raise original - - with pytest.raises(DppValidationClientError) as exc: - _make_generic_repo(handler, _EncodingFailureCodec(), validator).create_dpp(object()) - assert exc.value is original - assert not sent - - -def test_codec_encoding_failure_precedes_network() -> None: - sent = False - - def handler(request: httpx.Request) -> httpx.Response: - nonlocal sent - sent = True - return httpx.Response(200) - - with pytest.raises(DppMappingClientError) as exc: - _make_generic_repo(handler, _EncodingFailureCodec(), lambda value: None).create_dpp( - object() +def test_registry_mapping_failures_are_distinct_from_api_failures(body: Any) -> None: + registry, raw = client_for(DppRegistryClient, body) + if body["payload"] == {}: + # An absent registration ID is allowed by the Java response payload. + assert ( + registry.post_new_dpp_to_registry({"uniqueProductIdentifier": "p1"}).registration_id + is None ) - assert isinstance(exc.value.__cause__, ValueError) - assert not sent - - -def test_codec_null_serialization_is_a_causal_mapping_error_before_network() -> None: - sent = False - - def handler(request: httpx.Request) -> httpx.Response: - nonlocal sent - sent = True - return httpx.Response(200) - - with pytest.raises(DppMappingClientError, match="serialization") as exc: - _make_generic_repo(handler, _NullEncodingCodec(), lambda value: None).create_dpp(object()) - assert isinstance(exc.value.__cause__, ValueError) - assert str(exc.value.__cause__) == "codec returned null JSON" - assert not sent - - -@pytest.mark.parametrize( - ("error", "expected"), - [ - pytest.param(httpx.ReadTimeout("timeout"), "timeout", id="timeout"), - pytest.param(httpx.ConnectError("transport"), "transport", id="transport"), - ], -) -def test_httpx_failures_become_network_errors_with_causes(error: Exception, expected: str) -> None: - def handler(request: httpx.Request) -> httpx.Response: - raise error - - with pytest.raises(DppNetworkClientError) as exc: - _make_repo(handler).read_dpp_by_id("D") - assert exc.value.__cause__ is error - + else: + with pytest.raises(DppMappingClientError): + registry.post_new_dpp_to_registry({"uniqueProductIdentifier": "p1"}) + raw.close() -def test_2xx_envelope_mapping_and_payload_failures_remain_mapping_errors() -> None: - malformed = _make_repo(lambda request: httpx.Response(200, text="not-json")) - with pytest.raises(DppMappingClientError) as malformed_error: - malformed.read_dpp_by_id("D") - assert malformed_error.value.__cause__ is not None - missing_payload = _make_repo( - lambda request: httpx.Response(200, json={"statusCode": "Success"}) +def test_registry_response_uses_only_the_canonical_registration_id() -> None: + registry, raw = client_for( + DppRegistryClient, + {"statusCode": "Success", "payload": {"registryIdentifier": "legacy"}, "message": []}, ) - with pytest.raises(DppMappingClientError, match="payload"): - missing_payload.read_dpp_by_id("D") + response = registry.post_new_dpp_to_registry({"uniqueProductIdentifier": "p1"}) + assert response.registration_id is None -def test_missing_api_status_is_a_causal_mapping_error() -> None: - repo = _make_generic_repo( - lambda request: httpx.Response(200, json={"payload": {}}), - _NullDecodingCodec(), - lambda value: None, - ) - with pytest.raises(DppMappingClientError, match="statusCode") as exc: - repo.read_dpp_by_id("D") - assert isinstance(exc.value.__cause__, ValueError) - assert str(exc.value.__cause__) == "Missing required response field: statusCode" +def test_registry_204_empty_body_is_a_mapping_failure() -> None: + registry, raw = client_for(DppRegistryClient, "", status=204) + with pytest.raises(DppMappingClientError): + registry.post_new_dpp_to_registry({"uniqueProductIdentifier": "p1"}) + raw.close() + raw.close() @pytest.mark.parametrize( - ("missing_field", "operation"), + ("status", "body", "error"), [ - pytest.param( - "payload", - "read-dpp", - id="required-payload-helper", + (503, "offline", DppHttpClientError), + (503, {"statusCode": "ServerErrorBadGateway", "message": []}, DppApiClientError), + (200, "{", DppMappingClientError), + (200, {"payload": {}}, DppMappingClientError), + ( + 200, + { + "statusCode": "Success", + "payload": {}, + "message": [{"messageType": "Info", "timestamp": "2026-01-01T12:00:00"}], + }, + DppMappingClientError, ), - pytest.param( - "payload.dppId", - "create-dpp-id", - id="required-text-field-helper", + ( + 200, + { + "statusCode": "ClientErrorResourceNotFound", + "message": [{"messageType": "Error", "text": "gone"}], + }, + DppApiClientError, ), - pytest.param( - "payload.dppIdentifiers", - "read-dpp-identifiers", - id="required-field-helper", + ( + 200, + {"statusCode": "Success", "message": [{"messageType": "Info"}]}, + DppMappingClientError, ), - pytest.param( - "payload.registrationId", - "registry-registration-id", - id="registry-required-text-field-helper", + ( + 503, + {"statusCode": "ServerErrorBadGateway", "message": [{"text": "broken"}]}, + DppHttpClientError, ), ], ) -def test_synthesized_missing_structure_errors_have_meaningful_causes( - missing_field: str, - operation: str, -) -> None: - if operation == "read-dpp": - repo = _make_generic_repo( - lambda request: httpx.Response(200, json={"statusCode": "Success"}), - _NullDecodingCodec(), - lambda value: None, - ) - - def call() -> None: - repo.read_dpp_by_id("D") - - elif operation == "create-dpp-id": - repo = _make_generic_repo( - lambda request: httpx.Response( - 200, json={"statusCode": "SuccessCreated", "payload": {}} - ), - _NullDecodingCodec(), - lambda value: None, - ) - - def call() -> None: - repo.create_dpp(object()) - - elif operation == "read-dpp-identifiers": - repo = _make_generic_repo( - lambda request: httpx.Response(200, json={"statusCode": "Success", "payload": {}}), - _NullDecodingCodec(), - lambda value: None, - ) - - def call() -> None: - repo.read_dpp_ids_by_product_ids(["P"]) - - else: - registry = DppRegistryClient( - "http://reg.test", - client=httpx.Client( - transport=httpx.MockTransport( - lambda request: httpx.Response( - 200, json={"statusCode": "Success", "payload": {}} - ) - ) - ), - ) - - def call() -> None: - registry.post_new_dpp_to_registry(RegisterDppRequest()) +def test_response_error_families(status: int, body: Any, error: type[Exception]) -> None: + repo, raw = client_for(DppRepoClient, body, status) + with pytest.raises(error): + repo.read_dpp_by_id("d1") + raw.close() - with pytest.raises(DppMappingClientError, match=missing_field) as exc: - call() - assert isinstance(exc.value.__cause__, ValueError) - assert str(exc.value.__cause__) == f"Missing required response field: {missing_field}" +def test_network_and_timeout_errors_are_distinct() -> None: + def fail(request: httpx.Request) -> httpx.Response: + raise httpx.ConnectError("network down", request=request) -def test_codec_decoding_failure_becomes_mapping_error_with_cause() -> None: - repo = _make_generic_repo( - lambda request: httpx.Response(200, json={"statusCode": "Success", "payload": {}}), - _DecodingFailureCodec(), - lambda value: None, + repo = DppRepoClient( + "https://api.example", client=httpx.Client(transport=httpx.MockTransport(fail)) ) - with pytest.raises(DppMappingClientError) as exc: - repo.read_dpp_by_id("D") - assert isinstance(exc.value.__cause__, ValueError) + with pytest.raises(DppNetworkClientError): + repo.read_dpp_by_id("d1") + def timeout(request: httpx.Request) -> httpx.Response: + raise httpx.ReadTimeout("timed out", request=request) -def test_codec_null_result_is_a_causal_mapping_error_with_operation_context() -> None: - repo = _make_generic_repo( - lambda request: httpx.Response(200, json={"statusCode": "Success", "payload": {}}), - _NullDecodingCodec(), - lambda value: None, + repo = DppRepoClient( + "https://api.example", client=httpx.Client(transport=httpx.MockTransport(timeout)) ) - with pytest.raises(DppMappingClientError, match="deserialization") as exc: - repo.read_dpp_by_id("D") - assert isinstance(exc.value.__cause__, ValueError) - assert str(exc.value.__cause__) == "codec returned null DPP" - - -@pytest.mark.parametrize( - "contract_id", - [ - "CLIENT-VALIDATOR-PROTOCOL-001", - "VALIDATION-CLIENT-DPP-DPP-VALIDATOR-88261F", - "VALIDATION-CLIENT-DPP-VALIDATION-CLIENT-EXCEPTION-DPP-VALIDATION-CLIENT-EXCEPTION-99A38C", - ], -) -def test_client_validation_contracts_are_callable_and_preserve_category(contract_id: str) -> None: - original = DppValidationClientError("validation failed") + with pytest.raises(DppNetworkClientError, match="timed out"): + repo.read_dpp_by_id("d1") - def validator(value: object) -> None: - raise original - with pytest.raises(DppValidationClientError) as exc: - _make_generic_repo( - lambda request: httpx.Response(200), _EncodingFailureCodec(), validator - ).create_dpp(object()) - assert exc.value is original - - -@pytest.mark.parametrize( - ("contract_id", "error_type"), - [ - ("ERROR-DPP-CLIENT-001", DppClientError), - ("ERROR-DPP-MAPPING-CLIENT-001", DppMappingClientError), - ("ERROR-DPP-NETWORK-CLIENT-001", DppNetworkClientError), - ("ERROR-DPP-VALIDATION-CLIENT-001", DppValidationClientError), - ("ERROR-MAPPING-001", DppMappingError), - ], -) -def test_exception_contracts_expose_stable_categories( - contract_id: str, error_type: type[Exception] -) -> None: - error = error_type("contract context") - assert str(error) == "contract context" +def test_required_payload_and_context_ownership() -> None: + repo, raw = client_for(DppRepoClient, {"statusCode": "Success", "payload": None, "message": []}) + with pytest.raises(DppMappingClientError, match="payload"): + repo.read_data_element("d1", "$.name") + repo.close() + assert not raw.is_closed + raw.close() + + +def test_request_mapping_failure_happens_before_transport() -> None: + seen: list[httpx.Request] = [] + repo, raw = client_for(DppRepoClient, seen=seen) + with pytest.raises(DppMappingClientError): + repo.create_dpp({"unserializable": object()}) + with pytest.raises(TypeError): + repo.create_dpp(None) + assert seen == [] + raw.close() diff --git a/tests/test_core_json_codec.py b/tests/test_core_json_codec.py new file mode 100644 index 0000000..c253448 --- /dev/null +++ b/tests/test_core_json_codec.py @@ -0,0 +1,287 @@ +from __future__ import annotations + +import json +from datetime import UTC, datetime +from decimal import Decimal + +import pytest + +from dpp_sdk.core import ( + CompressedExpansionResult, + DataElementCollection, + DataElementKind, + DigitalProductPassport, + DppHeader, + DppJsonCodec, + DppJsonException, + Granularity, + MultiValuedDataElement, + RelatedResource, + SemanticDefinition, + SemanticPath, + SingleValuedDataElement, +) + + +def passport() -> DigitalProductPassport: + return DigitalProductPassport( + dpp_header=DppHeader( + digital_product_passport_id="https://example.test/dpp/1", + unique_product_identifier="https://example.test/product/1", + granularity=Granularity.ITEM, + dpp_schema_version="EN18223:2026", + dpp_status="active", + last_updated=datetime(2026, 9, 21, 10, 0, tzinfo=UTC), + economic_operator_id="urn:example:operator:1", + content_specification_ids=("SPEC-1",), + ), + data_elements=( + DataElementCollection( + element_id="details", + dictionary_reference="urn:dictionary:details", + elements=( + SingleValuedDataElement( + element_id="label", + dictionary_reference="urn:dictionary:label", + value_data_type="xsd:string", + value="Chair", + ), + ), + ), + MultiValuedDataElement( + element_id="weights", + value_data_type="xsd:decimal", + value=( + SingleValuedDataElement(element_id="first", value=1.5), + SingleValuedDataElement(element_id="second", value=2.5), + ), + ), + ), + ) + + +def test_full_codec_preserves_canonical_shape_and_semantic_metadata() -> None: + codec = DppJsonCodec() + source = passport() + + encoded = codec.to_expanded_json(source) + decoded = codec.from_expanded_json(encoded) + wire = json.loads(encoded) + + assert wire["granularity"] == "item" + assert list(wire)[-1] == "elements" + assert wire["elements"][0]["objectType"] == "DataElementCollection" + assert wire["elements"][0]["elements"][0]["valueDataType"] == "xsd:string" + assert decoded == source + + +def test_java_instant_millisecond_precision_is_canonical_on_the_wire() -> None: + codec = DppJsonCodec() + source = passport().with_updates( + dpp_header=passport().dpp_header.with_updates( + last_updated=datetime(2026, 9, 21, 10, 0, 0, 123_000, tzinfo=UTC) + ) + ) + + encoded = codec.to_expanded_json(source) + + assert json.loads(encoded)["lastUpdated"] == "2026-09-21T10:00:00.123Z" + assert codec.from_expanded_json(encoded) == source + + +def test_full_codec_preserves_decimal_value_scale_without_float_conversion() -> None: + codec = DppJsonCodec() + source = passport().with_updates( + data_elements=( + SingleValuedDataElement( + element_id="amount", + value_data_type="xsd:decimal", + value=Decimal("123.4500"), + ), + ) + ) + + encoded = codec.to_expanded_json(source) + decoded = codec.from_expanded_json(encoded) + + assert '"value":123.4500' in encoded + assert decoded.data_elements[0].value == Decimal("123.4500") + + +def test_compressed_expansion_reports_missing_metadata_without_inventing_it() -> None: + codec = DppJsonCodec() + compressed = codec.to_compressed_json(passport()) + + result = codec.expand_compressed_json(compressed) + expanded = result.passport.data_elements + + assert isinstance(result, CompressedExpansionResult) + assert isinstance(expanded[0], DataElementCollection) + assert expanded[0].dictionary_reference is None + assert isinstance(expanded[0].elements[0], SingleValuedDataElement) + assert expanded[0].elements[0].value_data_type is None + assert result.issues + assert json.loads(codec.to_compressed_json(result.passport)) == json.loads(compressed) + + +def test_compressed_expansion_uses_previous_passport_to_restore_known_metadata() -> None: + codec = DppJsonCodec() + source = passport() + + result = codec.expand_compressed_json(codec.to_compressed_json(source), previous=source) + + assert result.passport == source + + +def test_resolver_supplies_only_explicit_semantics_at_complete_paths() -> None: + codec = DppJsonCodec() + source = passport() + compressed = codec.to_compressed_json(source) + + class Resolver: + def resolve(self, content_specification_ids: tuple[str, ...], path: SemanticPath): + assert content_specification_ids == ("SPEC-1",) + definitions = { + SemanticPath(("details",)): SemanticDefinition( + kind=DataElementKind.COLLECTION, + element_id="details", + dictionary_reference="urn:dictionary:details", + ), + SemanticPath(("details", "label")): SemanticDefinition( + kind=DataElementKind.SINGLE, + element_id="label", + dictionary_reference="urn:dictionary:label", + value_data_type="xsd:string", + ), + SemanticPath(("weights",)): SemanticDefinition( + kind=DataElementKind.MULTI_VALUED, + element_id="weights", + value_data_type="xsd:decimal", + dictionary_reference_resolved=True, + ), + SemanticPath(("weights", "0")): SemanticDefinition( + kind=DataElementKind.SINGLE, + element_id="first", + value_data_type="xsd:decimal", + dictionary_reference_resolved=True, + ), + SemanticPath(("weights", "1")): SemanticDefinition( + kind=DataElementKind.SINGLE, + element_id="second", + value_data_type="xsd:decimal", + dictionary_reference_resolved=True, + ), + } + return definitions.get(path) + + result = codec.expand_compressed_json_with_resolver(compressed, Resolver()) + + resolved_weights = result.passport.data_elements[1] + assert isinstance(resolved_weights, MultiValuedDataElement) + assert resolved_weights.value_data_type == "xsd:decimal" + assert all( + isinstance(item, SingleValuedDataElement) and item.value_data_type == "xsd:decimal" + for item in resolved_weights.value + ) + assert result.passport.dpp_header == source.dpp_header + assert result.issues == () + + +def test_resolved_absence_conflicts_with_metadata_in_the_previous_passport() -> None: + codec = DppJsonCodec() + source = passport() + compressed = codec.to_compressed_json(source) + + class Resolver: + def resolve(self, content_specification_ids: tuple[str, ...], path: SemanticPath): + if path == SemanticPath(("details", "label")): + return SemanticDefinition( + kind=DataElementKind.SINGLE, + element_id="label", + dictionary_reference_resolved=True, + ) + return None + + with pytest.raises(DppJsonException, match="conflicts with stored dictionaryReference"): + codec.expand_compressed_json(compressed, previous=source, resolver=Resolver()) + + +@pytest.mark.parametrize("bad_json", ["{", "[]", '{"x": 1, "x": 2}']) +def test_compressed_codec_rejects_malformed_roots_and_duplicate_keys(bad_json: str) -> None: + with pytest.raises(DppJsonException): + DppJsonCodec().expand_compressed_json(bad_json) + + +def test_full_codec_rejects_incomplete_headers_and_unknown_element_types() -> None: + codec = DppJsonCodec() + valid_json = codec.to_expanded_json(passport()) + + with pytest.raises(DppJsonException, match="dppStatus"): + codec.from_expanded_json(valid_json.replace('"dppStatus":"active",', "")) + with pytest.raises(DppJsonException, match="unknown objectType 'FutureType'"): + codec.from_expanded_json(valid_json.replace("DataElementCollection", "FutureType")) + with pytest.raises(DppJsonException, match="digitalProductPassportId is required"): + codec.to_expanded_json(DigitalProductPassport(dpp_header=DppHeader())) + + +def test_arbitrary_compressed_element_id_does_not_gain_semantic_metadata() -> None: + codec = DppJsonCodec() + root = json.loads(codec.to_compressed_json(passport())) + root["unregistered-extension"] = "value" + + expanded = codec.expand_compressed_json(json.dumps(root)).passport + extension = expanded.data_elements[-1] + + assert extension.element_id == "unregistered-extension" + assert isinstance(extension, SingleValuedDataElement) + assert extension.dictionary_reference is None + assert extension.value_data_type is None + + +def test_element_codec_and_header_inspection_follow_the_java_operations() -> None: + codec = DppJsonCodec() + element = passport().data_elements[0] + + expanded = codec.to_expanded_element_json(element) + restored = codec.from_expanded_element_json(expanded) + compressed = codec.to_compressed_element_json(element) + header = codec.read_header(codec.to_expanded_json(passport())) + + assert restored == element + assert json.loads(compressed) == {"label": "Chair"} + assert header == passport().dpp_header + assert ( + codec.expand_compressed_element_json("label", '"Seat"', element.elements[0]).value == "Seat" + ) + + +def test_related_resource_field_order_matches_java_transport() -> None: + codec = DppJsonCodec() + resource = RelatedResource( + element_id="manual", + content_type="application/pdf", + url="https://example.test/manual.pdf", + language="en", + resource_title="Manual", + ) + + full_fields = list(json.loads(codec.to_expanded_element_json(resource))) + compressed_fields = list(json.loads(codec.to_compressed_element_json(resource))) + + assert full_fields == [ + "elementId", + "objectType", + "contentType", + "url", + "language", + "resourceTitle", + ] + assert compressed_fields == ["resourceTitle", "contentType", "url", "language"] + + +def test_codec_types_are_exported_from_the_package_root() -> None: + import dpp_sdk + + assert dpp_sdk.DppJsonCodec is DppJsonCodec + assert dpp_sdk.SemanticResolver is not None + assert dpp_sdk.CompressedExpansionResult is CompressedExpansionResult diff --git a/tests/test_core_model.py b/tests/test_core_model.py index fd3ff2c..37e697a 100644 --- a/tests/test_core_model.py +++ b/tests/test_core_model.py @@ -2,399 +2,199 @@ from __future__ import annotations -from datetime import date -from uuid import UUID +from datetime import UTC, datetime +from decimal import Decimal import pytest from pydantic import ValidationError from dpp_sdk.core.model import ( - Address, - Contact, - Documentation, - DppCore, - Email, - Nameplate, - Organization, - OrganizationRole, - PassportMetadata, - Telephone, + DataElementCollection, + DigitalProductPassport, + DppHeader, + Granularity, + RelatedResource, + SingleValuedDataElement, ) - - -def test_construction_and_identifiers(valid_core: DppCore) -> None: - assert valid_core.nameplate.gtinCode == "GTIN-0001" - assert ( - str(valid_core.passportMetadata.uniqueProductIdentifier) - == "11111111-1111-1111-1111-111111111111" +from dpp_sdk.core.semantic import SemanticPath + + +def test_dpp_header_preserves_optional_fields_and_ordered_immutable_specs() -> None: + specifications = ["spec-a", "spec-b"] + header = DppHeader.model_validate( + { + "digitalProductPassportId": "passport-id", + "uniqueProductIdentifier": "product-id", + "granularity": "item", + "dppSchemaVersion": "2026", + "dppStatus": "active", + "lastUpdated": "2026-01-02T03:04:05Z", + "economicOperatorId": "operator-id", + "facilityId": "facility-id", + "contentSpecificationIds": specifications, + } ) - - -def test_models_are_frozen(valid_core: DppCore) -> None: + specifications.append("late") + + assert header.digital_product_passport_id == "passport-id" + assert header.unique_product_identifier == "product-id" + assert header.granularity is Granularity.ITEM + assert header.dpp_schema_version == "2026" + assert header.dpp_status == "active" + assert header.last_updated == datetime(2026, 1, 2, 3, 4, 5, tzinfo=UTC) + assert header.economic_operator_id == "operator-id" + assert header.facility_id == "facility-id" + assert header.content_specification_ids == ("spec-a", "spec-b") + assert header == DppHeader(**header.model_dump()) + assert header.with_updates(facility_id=None).facility_id is None + assert header.facility_id == "facility-id" with pytest.raises(ValidationError): - valid_core.nameplate.gtinCode = "changed" # type: ignore[misc] - - -@pytest.mark.parametrize("whitespace", [" ", "\t", "\n", "\u00a0", "\u202f", "\u2003"]) -def test_dec_002_frozen_unicode_whitespace_is_blank(whitespace: str) -> None: - with pytest.raises(ValidationError, match="must not be blank"): - Address(country=whitespace, town="Town") - - -@pytest.mark.parametrize("visible", ["\u200b", "\u001c", "\u00a0visible\u2003"]) -def test_dec_002_non_table_or_mixed_text_is_visible(visible: str) -> None: - assert Address(country=visible, town="Town").country == visible - - -def test_with_updates_for_validated_edits(valid_core: DppCore) -> None: - updated = valid_core.nameplate.with_updates(batchNumber="B-9") - assert updated.batchNumber == "B-9" - assert valid_core.nameplate.batchNumber is None # original untouched - - -def test_json_round_trip_preserves_equality(valid_core: DppCore) -> None: - restored = DppCore.model_validate_json(valid_core.model_dump_json()) - assert restored == valid_core - - -def test_nulls_are_emitted_not_omitted(valid_core: DppCore) -> None: - dumped = valid_core.model_dump(mode="json") - # supplier was never set; Jackson parity requires the key present with null. - assert "supplier" in dumped["nameplate"] - assert dumped["nameplate"]["supplier"] is None - - -def test_uuid_and_date_serialize_as_strings(valid_core: DppCore) -> None: - meta = valid_core.model_dump(mode="json")["passportMetadata"] - assert meta["uniqueProductIdentifier"] == "11111111-1111-1111-1111-111111111111" - assert meta["passportUpdateDates"] == ["2024-01-01"] - - -def test_required_field_missing_raises() -> None: - with pytest.raises(ValidationError): - Nameplate() # type: ignore[call-arg] - - -def test_blank_required_string_raises() -> None: - with pytest.raises(ValidationError): - Nameplate(gtinCode=" ") - - -def test_blank_optional_string_raises() -> None: - with pytest.raises(ValidationError): - Organization(name="ACME", uri="") - - -def test_empty_update_dates_raises() -> None: - with pytest.raises(ValidationError): - PassportMetadata(uniqueProductIdentifier=UUID(int=1), passportUpdateDates=[]) + header.content_specification_ids += ("changed",) # type: ignore[misc] @pytest.mark.parametrize( - "contract_id", + "updates", [ - "MODEL-CORE-DOCUMENTATION", - "MODEL-CORE-DOCUMENTATION-CONSTRUCTION", - "MODEL-CORE-DOCUMENTATION-FIELD-PAPER-COPY-AVAILABLE-ON-REQUEST", - "MODEL-CORE-DOCUMENTATION-FIELD-SAFETY-INSTRUCTIONS-LINK", - "MODEL-CORE-DPP", - "MODEL-CORE-DPP-CORE", - "MODEL-CORE-DPP-CORE-CONSTRUCTION", - "MODEL-CORE-NAMEPLATE", - "MODEL-CORE-NAMEPLATE-CONSTRUCTION", - "MODEL-CORE-NAMEPLATE-FIELD-CUSTOMS-TARIFF-NUMBER", - "MODEL-CORE-NAMEPLATE-FIELD-URI-OF-THE-PRODUCT", - "MODEL-CORE-ORGANIZATION", - "MODEL-CORE-ORGANIZATION-CONSTRUCTION", - "MODEL-CORE-ORGANIZATION-FIELD-GLN", - "MODEL-CORE-ORGANIZATION-FIELD-PRODUCT-DESCRIPTION", - "MODEL-CORE-ORGANIZATION-FIELD-PRODUCT-DESIGNATION", - "MODEL-CORE-ORGANIZATION-FIELD-PRODUCT-FAMILY", - "MODEL-CORE-ORGANIZATION-FIELD-PRODUCT-ORDER-SUFFIX", - "MODEL-CORE-ORGANIZATION-FIELD-PRODUCT-ROOT", - "MODEL-CORE-ORGANIZATION-FIELD-URI", - "MODEL-CORE-ORGANIZATION-ROLE", - "MODEL-CORE-ORGANIZATION-ROLE-FIELD-DISTRIBUTOR", + {"content_specification_ids": None}, + {"content_specification_ids": ("valid", None)}, + {"last_updated": datetime(2026, 1, 2)}, + {"unknown": "field"}, ], - ids=lambda value: value, ) -def test_remaining_core_contract_evidence(contract_id: str, valid_core: DppCore) -> None: - """Each ID executes construction, immutability and JSON round-trip evidence.""" - model = ( - valid_core - if "DPP-CORE" in contract_id - else ( - valid_core.documentation - if "DOCUMENTATION" in contract_id - else valid_core.nameplate - if "NAMEPLATE" in contract_id - else valid_core.nameplate.manufacturer - ) - ) - restored = type(model).model_validate(model.model_dump(mode="json")) - assert restored == model +def test_dpp_header_rejects_structurally_invalid_updates(updates: dict[str, object]) -> None: with pytest.raises(ValidationError): - model.with_updates(**{"unexpected": "value"}) - - -def test_address_requires_country_and_town() -> None: - with pytest.raises(ValidationError): - Address(country="DE", town="") - - -def test_unknown_field_forbidden() -> None: - with pytest.raises(ValidationError): - Organization(name="ACME", role=OrganizationRole.SUPPLIER, surprise="x") # type: ignore[call-arg] - + DppHeader().with_updates(**updates) + + +def test_digital_product_passport_requires_header_and_parses_nested_full_elements() -> None: + full_elements = [ + { + "objectType": "DataElementCollection", + "elementId": "identity", + "elements": [ + { + "objectType": "SingleValuedDataElement", + "elementId": "name", + "dictionaryReference": "dictionary:name", + "value": "chair", + "valueDataType": "xsd:string", + } + ], + }, + { + "objectType": "RelatedResource", + "elementId": "manual", + "contentType": "application/pdf", + "url": "https://example.com/manual.pdf", + "language": "en", + "resourceTitle": "Manual", + }, + ] + passport = DigitalProductPassport.model_validate( + { + "dpp_header": {"digitalProductPassportId": "passport"}, + "elements": full_elements, + } + ) -def test_future_date_allowed_at_construction() -> None: - # Structural tier is lenient about future dates; only the validator rejects them. - meta = PassportMetadata( - uniqueProductIdentifier=UUID(int=2), passportUpdateDates=[date(2999, 1, 1)] + assert passport.dpp_header.digital_product_passport_id == "passport" + assert isinstance(passport.data_elements[0], DataElementCollection) + nested = passport.data_elements[0].elements[0] + assert isinstance(nested, SingleValuedDataElement) + assert nested.object_type == "SingleValuedDataElement" + assert nested.dictionary_reference == "dictionary:name" + assert nested.value == "chair" + assert isinstance(passport.data_elements[1], RelatedResource) + assert passport.data_elements[1].resource_title == "Manual" + assert len(passport.data_elements) == 2 + assert DigitalProductPassport(dpp_header=passport.dpp_header) == DigitalProductPassport( + dpp_header=passport.dpp_header, data_elements=() ) - assert meta.passportUpdateDates == (date(2999, 1, 1),) @pytest.mark.parametrize( - ("contract_id", "factory"), + "data", [ - ("MODEL-CORE-ADDRESS", lambda: Address(country="DE", town="Berlin")), - ("MODEL-CORE-EMAIL", lambda: Email(emailAddress="info@example.test")), - ("MODEL-CORE-TELEPHONE", lambda: Telephone(telephoneNumber="+49-30")), - ("MODEL-CORE-CONTACT", lambda: Contact(organization="ACME")), + {}, + {"dpp_header": None}, + {"dpp_header": {}, "elements": [{"objectType": "UnknownElement"}]}, + { + "dpp_header": {}, + "elements": [{"objectType": "SingleValuedDataElement", "extensionId": "unsupported"}], + }, + { + "dpp_header": {}, + "elements": [{"objectType": "SingleValuedDataElement", "value": object()}], + }, ], ) -def test_core_leaf_models_are_frozen_and_value_equal(contract_id: str, factory: object) -> None: - value = factory() # type: ignore[operator] - assert value == factory() # type: ignore[operator] +def test_digital_product_passport_rejects_missing_header_and_invalid_elements( + data: dict[str, object], +) -> None: with pytest.raises(ValidationError): - value.__setattr__(next(iter(type(value).model_fields)), "changed") - - -@pytest.mark.parametrize( - ("contract_id", "factory"), - [ - ("MODEL-CORE-ADDRESS-CONSTRUCTION", lambda: Address(country="DE", town="Berlin")), - ("MODEL-CORE-EMAIL-CONSTRUCTION", lambda: Email(emailAddress="info@example.test")), - ("MODEL-CORE-TELEPHONE-CONSTRUCTION", lambda: Telephone(telephoneNumber="+49-30")), - ("MODEL-CORE-CONTACT-CONSTRUCTION", lambda: Contact(organization="ACME")), - ], -) -def test_core_leaf_models_reject_unknown_fields(contract_id: str, factory: object) -> None: + DigitalProductPassport.model_validate(data) + + +def test_digital_product_passport_copies_order_and_revalidates_immutable_updates() -> None: + header = DppHeader(digital_product_passport_id="passport") + first = SingleValuedDataElement(element_id="first", value="one") + second = SingleValuedDataElement(element_id="second", value="two") + source = [first, second] + passport = DigitalProductPassport(dpp_header=header, data_elements=source) + source.clear() + + updated = passport.with_updates(data_elements=[second]) + assert passport.data_elements == (first, second) + assert updated.data_elements == (second,) + assert updated.dpp_header == header + assert updated == DigitalProductPassport(dpp_header=header, data_elements=[second]) + assert updated != passport with pytest.raises(ValidationError): - factory().__class__.model_validate({**factory().model_dump(), "unknown": True}) # type: ignore[operator] - - -@pytest.mark.parametrize( - ("contract_id", "input_dates"), - [ - ("MODEL-CORE-PASSPORT-METADATA", [date(2024, 1, 1)]), - ("MODEL-CORE-PASSPORT-METADATA-CONSTRUCTION", (date(2024, 1, 1),)), - ], -) -def test_passport_update_dates_are_immutable_json_arrays( - contract_id: str, input_dates: object -) -> None: - metadata = PassportMetadata( - uniqueProductIdentifier=UUID(int=1), passportUpdateDates=input_dates - ) # type: ignore[arg-type] - assert metadata.passportUpdateDates == (date(2024, 1, 1),) - assert metadata.model_dump(mode="json")["passportUpdateDates"] == ["2024-01-01"] - assert '"passportUpdateDates":["2024-01-01"]' in metadata.model_dump_json() - with pytest.raises(AttributeError): - metadata.passportUpdateDates.append(date(2024, 1, 2)) # type: ignore[attr-defined] - - -@pytest.mark.parametrize( - ("contract_id", "model", "changes"), - [ - ( - "MODEL-CORE-ADDRESS-IMMUTABLE-UPDATE", - Address(country="DE", town="Berlin"), - {"town": "Bonn"}, - ), - ( - "MODEL-CORE-CONTACT-IMMUTABLE-UPDATE", - Contact(organization="ACME"), - {"organization": "Other"}, - ), - ("MODEL-CORE-DOCUMENTATION-IMMUTABLE-UPDATE", Documentation(), {"downloadable": True}), - ("MODEL-CORE-DPP-CORE-IMMUTABLE-UPDATE", None, {}), - ( - "MODEL-CORE-EMAIL-IMMUTABLE-UPDATE", - Email(emailAddress="a@b.test"), - {"typeOfEmail": "work"}, - ), - ( - "MODEL-CORE-NAMEPLATE-IMMUTABLE-UPDATE", - Nameplate(gtinCode="GTIN"), - {"batchNumber": "B-1"}, - ), - ( - "MODEL-CORE-ORGANIZATION-IMMUTABLE-UPDATE", - Organization(name="ACME"), - {"uri": "https://example.test"}, - ), - ( - "MODEL-CORE-PASSPORT-METADATA-IMMUTABLE-UPDATE", - PassportMetadata( - uniqueProductIdentifier=UUID(int=1), passportUpdateDates=[date(2024, 1, 1)] - ), - {"qrCodeOrDigitalTag": "QR"}, - ), - ( - "MODEL-CORE-TELEPHONE-IMMUTABLE-UPDATE", - Telephone(telephoneNumber="1"), - {"typeOfTelephone": "work"}, - ), - ], -) -def test_with_updates_revalidates_and_preserves_concrete_type( - contract_id: str, model: object, changes: dict[str, object], valid_core: DppCore -) -> None: - value = valid_core if model is None else model - updated = value.with_updates(**changes) # type: ignore[union-attr] - assert type(updated) is type(value) - assert updated is not value - assert value.model_dump() != updated.model_dump() or not changes # type: ignore[union-attr] + passport.with_updates(dpp_header=None) with pytest.raises(ValidationError): - value.with_updates(unknown=True) # type: ignore[union-attr] + passport.with_updates(data_elements=[{"objectType": "UnknownElement"}]) -def test_with_updates_rejects_invalid_known_field_values() -> None: - """Updates must use model validation rather than unchecked copying.""" - nameplate = Nameplate(gtinCode="GTIN") +def test_digital_product_passport_finds_nested_elements_by_id_or_child_index() -> None: + leaf = SingleValuedDataElement(element_id="name", value="chair") + inner = DataElementCollection(element_id="details", elements=[leaf]) + outer = DataElementCollection(element_id="product", elements=[inner]) + passport = DigitalProductPassport(dpp_header=DppHeader(), data_elements=[outer]) - with pytest.raises(ValidationError, match="must not be blank if provided"): - nameplate.with_updates(batchNumber=" ") + assert passport.find_data_element(("product", "details", "name")) == leaf + assert passport.find_data_element(("product", "0", "0")) == leaf + assert passport.find_data_element(SemanticPath.of("product", "details", "name")) == leaf + assert passport.find_data_element(SemanticPath.of("product", "0", "0")) == leaf + assert passport.find_data_element(("product", "5")) is None + assert passport.find_data_element(("product", "name")) is None + with pytest.raises(ValueError, match="non-blank"): + passport.find_data_element(()) @pytest.mark.parametrize( - ( - "contract_id", - "model", - "valid_changes", - "changed_field", - "expected_value", - "invalid_changes", - "invalid_match", - ), + ("left", "right"), [ - pytest.param( - "MODEL-CORE-ADDRESS-IMMUTABLE-UPDATE", - Address(country="DE", town="Berlin"), - {"town": "Bonn"}, - "town", - "Bonn", - {"country": " "}, - "must not be blank", - id="MODEL-CORE-ADDRESS-IMMUTABLE-UPDATE", - ), - pytest.param( - "MODEL-CORE-CONTACT-IMMUTABLE-UPDATE", - Contact(organization="ACME"), - {"organization": "Other"}, - "organization", - "Other", - {"organization": " "}, - "must not be blank", - id="MODEL-CORE-CONTACT-IMMUTABLE-UPDATE", - ), - pytest.param( - "MODEL-CORE-DOCUMENTATION-IMMUTABLE-UPDATE", - Documentation(), - {"downloadable": True}, - "downloadable", - True, - {"availableForYears": -1}, - "greater than or equal to 0", - id="MODEL-CORE-DOCUMENTATION-IMMUTABLE-UPDATE", - ), - pytest.param( - "MODEL-CORE-DPP-CORE-IMMUTABLE-UPDATE", - None, - {"documentation": None}, - "documentation", - None, - {"nameplate": None}, - "nameplate", - id="MODEL-CORE-DPP-CORE-IMMUTABLE-UPDATE", - ), - pytest.param( - "MODEL-CORE-EMAIL-IMMUTABLE-UPDATE", - Email(emailAddress="info@example.test"), - {"typeOfEmail": "work"}, - "typeOfEmail", - "work", - {"emailAddress": " "}, - "must not be blank", - id="MODEL-CORE-EMAIL-IMMUTABLE-UPDATE", - ), - pytest.param( - "MODEL-CORE-NAMEPLATE-IMMUTABLE-UPDATE", - Nameplate(gtinCode="GTIN"), - {"batchNumber": "B-1"}, - "batchNumber", - "B-1", - {"batchNumber": " "}, - "must not be blank if provided", - id="MODEL-CORE-NAMEPLATE-IMMUTABLE-UPDATE", - ), - pytest.param( - "MODEL-CORE-ORGANIZATION-IMMUTABLE-UPDATE", - Organization(name="ACME"), - {"uri": "https://example.test"}, - "uri", - "https://example.test", - {"name": " "}, - "must not be blank", - id="MODEL-CORE-ORGANIZATION-IMMUTABLE-UPDATE", - ), - pytest.param( - "MODEL-CORE-PASSPORT-METADATA-IMMUTABLE-UPDATE", - PassportMetadata( - uniqueProductIdentifier=UUID(int=1), - passportUpdateDates=(date(2024, 1, 1),), - ), - {"qrCodeOrDigitalTag": "QR"}, - "qrCodeOrDigitalTag", - "QR", - {"passportUpdateDates": ()}, - "at least 1", - id="MODEL-CORE-PASSPORT-METADATA-IMMUTABLE-UPDATE", - ), - pytest.param( - "MODEL-CORE-TELEPHONE-IMMUTABLE-UPDATE", - Telephone(telephoneNumber="+49-30"), - {"typeOfTelephone": "work"}, - "typeOfTelephone", - "work", - {"telephoneNumber": " "}, - "must not be blank", - id="MODEL-CORE-TELEPHONE-IMMUTABLE-UPDATE", - ), + (0.1, Decimal("0.1")), + (1, Decimal("1.00")), + ([0.1, {"nested": 2}], [Decimal("0.1"), {"nested": Decimal("2.0")}]), ], ) -def test_core_contract_with_updates_revalidates_known_fields_and_preserves_original( - contract_id: str, - model: object, - valid_changes: dict[str, object], - changed_field: str, - expected_value: object, - invalid_changes: dict[str, object], - invalid_match: str, - valid_core: DppCore, -) -> None: - value = valid_core if model is None else model - before = value.model_dump(mode="python") # type: ignore[union-attr] - original_field = getattr(value, changed_field) +def test_single_value_numeric_equality_matches_java_recursively(left, right) -> None: + first = SingleValuedDataElement(element_id="value", value=left) + second = SingleValuedDataElement(element_id="value", value=right) + + assert first == second + assert hash(first) == hash(second) + assert DigitalProductPassport(dpp_header=DppHeader(), data_elements=[first]) == ( + DigitalProductPassport(dpp_header=DppHeader(), data_elements=[second]) + ) + - updated = value.with_updates(**valid_changes) # type: ignore[union-attr] +def test_new_aggregate_models_are_publicly_exported() -> None: + from dpp_sdk import DigitalProductPassport as RootPassport + from dpp_sdk import DppHeader as RootDppHeader + from dpp_sdk.core import DigitalProductPassport as CorePassport + from dpp_sdk.core import DppHeader as CoreDppHeader - assert type(updated) is type(value) - assert getattr(updated, changed_field) == expected_value - assert getattr(value, changed_field) == original_field - assert value.model_dump(mode="python") == before # type: ignore[union-attr] - with pytest.raises(ValidationError, match=invalid_match): - value.with_updates(**invalid_changes) # type: ignore[union-attr] - assert value.model_dump(mode="python") == before # type: ignore[union-attr] + assert RootDppHeader is CoreDppHeader is DppHeader + assert RootPassport is CorePassport is DigitalProductPassport diff --git a/tests/test_core_validation.py b/tests/test_core_validation.py deleted file mode 100644 index 2894846..0000000 --- a/tests/test_core_validation.py +++ /dev/null @@ -1,484 +0,0 @@ -"""Semantic validation of the core model (fail-fast, explicit).""" - -from __future__ import annotations - -from collections.abc import Callable -from datetime import date, timedelta -from typing import Any -from uuid import UUID - -import pytest -from pydantic import ValidationError - -from dpp_sdk.core.errors import DppError, DppValidationError -from dpp_sdk.core.model import ( - Address, - Contact, - Documentation, - DppCore, - Email, - Nameplate, - Organization, - OrganizationRole, - PassportMetadata, - Telephone, -) -from dpp_sdk.core.validation import ( - validate_address, - validate_contact, - validate_documentation, - validate_dpp_core, - validate_email, - validate_nameplate, - validate_organization, - validate_passport_metadata, - validate_telephone, -) - - -@pytest.mark.parametrize( - "contract_id, factory", - [ - ( - "VALIDATION-CORE-DOCUMENTATION-SAFETY-INSTRUCTIONS-LINK-MUST-NOT-BE-BLANK-IF-PROVIDED-F24E7A", - lambda: Documentation(safetyInstructionsLink=" "), - ), - ( - "VALIDATION-CORE-NAMEPLATE-CUSTOMS-TARIFF-NUMBER-MUST-NOT-BE-BLANK-IF-PROVIDED-BC8DDB", - lambda: Nameplate(gtinCode="G", customsTariffNumber=" "), - ), - ( - "VALIDATION-CORE-NAMEPLATE-URI-OF-THE-PRODUCT-MUST-NOT-BE-BLANK-IF-PROVIDED-BD396E", - lambda: Nameplate(gtinCode="G", uriOfTheProduct=" "), - ), - ( - "VALIDATION-CORE-ORGANIZATION-PRODUCT-DESCRIPTION-MUST-NOT-BE-BLANK-IF-PROVIDED-72C9DA", - lambda: Organization(name="A", productDescription=" "), - ), - ( - "VALIDATION-CORE-ORGANIZATION-PRODUCT-DESIGNATION-MUST-NOT-BE-BLANK-IF-PROVIDED-31B8FD", - lambda: Organization(name="A", productDesignation=" "), - ), - ( - "VALIDATION-CORE-ORGANIZATION-PRODUCT-FAMILY-MUST-NOT-BE-BLANK-IF-PROVIDED-4F1CD8", - lambda: Organization(name="A", productFamily=" "), - ), - ( - "VALIDATION-CORE-ORGANIZATION-PRODUCT-ORDER-SUFFIX-MUST-NOT-BE-BLANK-IF-PROVIDED-E0A619", - lambda: Organization(name="A", productOrderSuffix=" "), - ), - ( - "VALIDATION-CORE-ORGANIZATION-PRODUCT-ROOT-MUST-NOT-BE-BLANK-IF-PROVIDED-B78AEF", - lambda: Organization(name="A", productRoot=" "), - ), - ], - ids=lambda row: row if isinstance(row, str) else None, -) -def test_optional_blank_contract_evidence(contract_id: str, factory: Callable[[], object]) -> None: - with pytest.raises(ValidationError): - factory() - - -def test_valid_core_passes(valid_core: DppCore) -> None: - validate_dpp_core(valid_core) # no raise - - -def test_future_date_rejected(valid_core: DppCore) -> None: - future = date.today() + timedelta(days=1) - bad = valid_core.model_copy( - update={ - "passportMetadata": valid_core.passportMetadata.model_copy( - update={"passportUpdateDates": (future,)} - ) - } - ) - with pytest.raises(DppValidationError, match="future"): - validate_dpp_core(bad) - - -def test_nameplate_requires_manufacturer_or_supplier() -> None: - with pytest.raises(DppValidationError, match="manufacturer or supplier"): - validate_nameplate(Nameplate(gtinCode="G1")) - - -def test_manufacturer_role_must_match_slot() -> None: - np = Nameplate( - gtinCode="G1", manufacturer=Organization(name="ACME", role=OrganizationRole.SUPPLIER) - ) - with pytest.raises(DppValidationError, match="MANUFACTURER"): - validate_nameplate(np) - - -def test_manufacturer_role_missing_rejected() -> None: - np = Nameplate(gtinCode="G1", manufacturer=Organization(name="ACME")) - with pytest.raises(DppValidationError, match="role is null"): - validate_nameplate(np) - - -def test_email_must_contain_at() -> None: - with pytest.raises(DppValidationError, match="missing @"): - validate_email(Email(emailAddress="not-an-email")) - - -def test_contact_requires_a_channel() -> None: - with pytest.raises(DppValidationError, match="at least one contact channel"): - validate_contact(Contact(organization="HQ")) - - -def test_documentation_downloadable_requires_link() -> None: - with pytest.raises(DppValidationError, match="downloadable"): - validate_documentation(Documentation(downloadable=True)) - - -def test_documentation_available_years_requires_link() -> None: - with pytest.raises(DppValidationError, match="availableForYears"): - validate_documentation(Documentation(availableForYears=5)) - - -def test_external_doc_link_present_is_fine_when_constructed() -> None: - # Construction allows externalDocumentationLink; the cross-rule lives in dpp4fun. - meta = PassportMetadata( - uniqueProductIdentifier=UUID(int=1), - passportUpdateDates=(date(2024, 1, 1),), - externalDocumentationLink="https://x.example", - ) - assert meta.externalDocumentationLink == "https://x.example" - - -@pytest.mark.parametrize( - ("contract_id", "validator", "value", "context"), - [ - pytest.param( - "VALIDATION-CORE-ADDRESS-ADDRESS-COUNTRY-9CDFA3", - validate_address, - Address.model_construct(country="", town="Berlin"), - "Address.country", - id="VALIDATION-CORE-ADDRESS-ADDRESS-COUNTRY-9CDFA3", - ), - pytest.param( - "VALIDATION-CORE-ADDRESS-ADDRESS-TOWN-07F97D", - validate_address, - Address.model_construct(country="DE", town=""), - "Address.town", - id="VALIDATION-CORE-ADDRESS-ADDRESS-TOWN-07F97D", - ), - pytest.param( - "VALIDATION-CORE-ADDRESS-ADDRESS-ZIP-CODE-58EF55", - validate_address, - Address.model_construct(country="DE", town="Berlin", zipCode=""), - "Address.zipCode", - id="VALIDATION-CORE-ADDRESS-ADDRESS-ZIP-CODE-58EF55", - ), - pytest.param( - "VALIDATION-CORE-ADDRESS-ADDRESS-REGION-0D098C", - validate_address, - Address.model_construct(country="DE", town="Berlin", region=""), - "Address.region", - id="VALIDATION-CORE-ADDRESS-ADDRESS-REGION-0D098C", - ), - pytest.param( - "VALIDATION-CORE-ADDRESS-ADDRESS-STREET-7432C5", - validate_address, - Address.model_construct(country="DE", town="Berlin", street=""), - "Address.street", - id="VALIDATION-CORE-ADDRESS-ADDRESS-STREET-7432C5", - ), - pytest.param( - "VALIDATION-CORE-EMAIL-EMAIL-VALIDATOR-ACD929", - validate_email, - Email.model_construct(emailAddress="bad"), - "missing @", - id="VALIDATION-CORE-EMAIL-EMAIL-VALIDATOR-ACD929", - ), - pytest.param( - "VALIDATION-CORE-TELEPHONE-TELEPHONE-TELEPHONE-NUMBER-62DD93", - validate_telephone, - Telephone.model_construct(telephoneNumber=""), - "Telephone.telephoneNumber", - id="VALIDATION-CORE-TELEPHONE-TELEPHONE-TELEPHONE-NUMBER-62DD93", - ), - pytest.param( - "VALIDATION-CORE-ORGANIZATION-ORGANIZATION-NAME-6061B8", - validate_organization, - Organization.model_construct(name=""), - "Organization.name", - id="VALIDATION-CORE-ORGANIZATION-ORGANIZATION-NAME-6061B8", - ), - pytest.param( - "VALIDATION-CORE-TELEPHONE-TELEPHONE-TYPE-OF-TELEPHONE-E23B71", - validate_telephone, - Telephone.model_construct(telephoneNumber="+49", typeOfTelephone=" "), - "Telephone.typeOfTelephone", - id="VALIDATION-CORE-TELEPHONE-TELEPHONE-TYPE-OF-TELEPHONE-E23B71", - ), - pytest.param( - "VALIDATION-CORE-ORGANIZATION-ORGANIZATION-URI-024597", - validate_organization, - Organization.model_construct(name="ACME", uri=""), - "Organization.uri", - id="VALIDATION-CORE-ORGANIZATION-ORGANIZATION-URI-024597", - ), - ], -) -def test_core_leaf_validators_fail_fast_without_mutation( - contract_id: str, validator: Callable[[Any], None], value: Any, context: str -) -> None: - before = value.model_dump() - with pytest.raises(DppValidationError, match=context): - validator(value) - assert value.model_dump() == before - - -@pytest.mark.parametrize( - ("contract_id", "validator", "value"), - [ - pytest.param( - "VALIDATION-CORE-ADDRESS-ADDRESS-VALIDATOR-CCD6BD", - validate_address, - Address(country="DE", town="Berlin"), - id="VALIDATION-CORE-ADDRESS-ADDRESS-VALIDATOR-CCD6BD", - ), - pytest.param( - "VALIDATION-CORE-CONTACT-CONTACT-VALIDATOR-CD435C", - validate_contact, - Contact(organization="ACME", email=Email(emailAddress="info@example.test")), - id="VALIDATION-CORE-CONTACT-CONTACT-VALIDATOR-CD435C", - ), - pytest.param( - "VALIDATION-CORE-ORGANIZATION-ORGANIZATION-VALIDATOR-F89B91", - validate_organization, - Organization(name="ACME", uri="https://example.test"), - id="VALIDATION-CORE-ORGANIZATION-ORGANIZATION-VALIDATOR-F89B91", - ), - pytest.param( - "VALIDATION-CORE-TELEPHONE-TELEPHONE-VALIDATOR-2C430B", - validate_telephone, - Telephone(telephoneNumber="+49 30 123"), - id="VALIDATION-CORE-TELEPHONE-TELEPHONE-VALIDATOR-2C430B", - ), - ], -) -def test_core_leaf_validators_accept_valid_values_repeatedly( - contract_id: str, validator: Callable[[Any], None], value: Any -) -> None: - before = value.model_dump() - validator(value) - validator(value) - assert value.model_dump() == before - - -@pytest.mark.parametrize( - "contract_id", - [ - pytest.param( - "VALIDATION-CORE-ORGANIZATION-ORGANIZATION-IS-REQUIRED-01893A", - id="VALIDATION-CORE-ORGANIZATION-ORGANIZATION-IS-REQUIRED-01893A", - ) - ], -) -def test_organization_is_required(contract_id: str) -> None: - with pytest.raises(DppValidationError, match="Organization is required"): - validate_organization(None) - - -def test_core_aggregate_order_metadata_then_nameplate_then_documentation( - valid_core: DppCore, -) -> None: - metadata = PassportMetadata.model_construct( - uniqueProductIdentifier=None, passportUpdateDates=() - ) - core = DppCore.model_construct( - passportMetadata=metadata, - nameplate=Nameplate.model_construct(gtinCode=""), - documentation=Documentation(downloadable=True), - ) - with pytest.raises(DppValidationError, match="PassportMetadata"): - validate_dpp_core(core) - core = DppCore.model_construct( - passportMetadata=valid_core.passportMetadata, - nameplate=Nameplate.model_construct(gtinCode=""), - documentation=Documentation(downloadable=True), - ) - with pytest.raises(DppValidationError, match="Nameplate.gtinCode"): - validate_dpp_core(core) - core = DppCore.model_construct( - passportMetadata=valid_core.passportMetadata, - nameplate=valid_core.nameplate, - documentation=Documentation(downloadable=True), - ) - with pytest.raises(DppValidationError, match="downloadable"): - validate_dpp_core(core) - - -@pytest.mark.parametrize( - ("contract_id", "validator", "value"), - [ - pytest.param( - "VALIDATION-CORE-DOCUMENTATION-DOCUMENTATION-VALIDATOR-025B6C", - validate_documentation, - Documentation(digitalInstructionsLink="https://example.test/manual"), - id="VALIDATION-CORE-DOCUMENTATION-DOCUMENTATION-VALIDATOR-025B6C", - ), - ], -) -def test_core_aggregate_validators_accept_valid_values( - contract_id: str, validator: Callable[[Any], None], value: Any -) -> None: - before = value.model_dump() - validator(value) - validator(value) - assert value.model_dump() == before - - -@pytest.mark.parametrize( - "contract_id", - [ - pytest.param( - "VALIDATION-CORE-DPP-CORE-DPP-CORE-VALIDATOR-7DA7A0", - id="VALIDATION-CORE-DPP-CORE-DPP-CORE-VALIDATOR-7DA7A0", - ), - pytest.param( - "VALIDATION-CORE-GENERAL-VALIDATION-SERVICE-F1BAD8", - id="VALIDATION-CORE-GENERAL-VALIDATION-SERVICE-F1BAD8", - ), - pytest.param( - "VALIDATION-CORE-GENERAL-VALIDATOR-5967A0", - id="VALIDATION-CORE-GENERAL-VALIDATOR-5967A0", - ), - ], -) -def test_dpp_core_validator_accepts_valid_value_repeatedly( - contract_id: str, valid_core: DppCore -) -> None: - before = valid_core.model_dump() - validate_dpp_core(valid_core) - validate_dpp_core(valid_core) - assert valid_core.model_dump() == before - - -@pytest.mark.parametrize( - ("contract_id", "value", "context"), - [ - pytest.param( - "VALIDATION-CORE-PASSPORT-METADATA-PASSPORT-METADATA-IS-REQUIRED-D184CB", - None, - "PassportMetadata is required", - id="VALIDATION-CORE-PASSPORT-METADATA-PASSPORT-METADATA-IS-REQUIRED-D184CB", - ), - pytest.param( - "VALIDATION-CORE-PASSPORT-METADATA-PASSPORT-METADATA-UNIQUE-PRODUCT-IDENTIFIER-FAF1E3", - PassportMetadata.model_construct(uniqueProductIdentifier=None, passportUpdateDates=()), - "uniqueProductIdentifier", - id="VALIDATION-CORE-PASSPORT-METADATA-PASSPORT-METADATA-UNIQUE-PRODUCT-IDENTIFIER-FAF1E3", - ), - pytest.param( - "VALIDATION-CORE-PASSPORT-METADATA-PASSPORT-METADATA-PASSPORT-UPDATE-DATES-MUST-NOT-BE-EMP-6E5F21", - PassportMetadata.model_construct( - uniqueProductIdentifier=UUID(int=1), passportUpdateDates=() - ), - "must not be empty", - id="VALIDATION-CORE-PASSPORT-METADATA-PASSPORT-METADATA-PASSPORT-UPDATE-DATES-MUST-NOT-BE-EMP-6E5F21", - ), - pytest.param( - "VALIDATION-CORE-PASSPORT-METADATA-PASSPORT-METADATA-PASSPORT-UPDATE-DATES-5A79C5", - PassportMetadata.model_construct( - uniqueProductIdentifier=UUID(int=1), passportUpdateDates=(None,) - ), - "passportUpdateDates\\[0\\] is null", - id="VALIDATION-CORE-PASSPORT-METADATA-PASSPORT-METADATA-PASSPORT-UPDATE-DATES-5A79C5", - ), - pytest.param( - "VALIDATION-CORE-PASSPORT-METADATA-PASSPORT-METADATA-PASSPORT-UPDATE-DATES-656933", - PassportMetadata.model_construct( - uniqueProductIdentifier=UUID(int=1), - passportUpdateDates=(date.today() + timedelta(days=1),), - ), - "is in the future", - id="VALIDATION-CORE-PASSPORT-METADATA-PASSPORT-METADATA-PASSPORT-UPDATE-DATES-656933", - ), - pytest.param( - "VALIDATION-CORE-PASSPORT-METADATA-PASSPORT-METADATA-QR-CODE-OR-DIGITAL-TAG-B484DD", - PassportMetadata.model_construct( - uniqueProductIdentifier=UUID(int=1), - passportUpdateDates=(date(2024, 1, 1),), - qrCodeOrDigitalTag=" ", - ), - "qrCodeOrDigitalTag", - id="VALIDATION-CORE-PASSPORT-METADATA-PASSPORT-METADATA-QR-CODE-OR-DIGITAL-TAG-B484DD", - ), - pytest.param( - "VALIDATION-CORE-PASSPORT-METADATA-PASSPORT-METADATA-EXTERNAL-DOCUMENTATION-LINK-50F759", - PassportMetadata.model_construct( - uniqueProductIdentifier=UUID(int=1), - passportUpdateDates=(date(2024, 1, 1),), - externalDocumentationLink=" ", - ), - "externalDocumentationLink", - id="VALIDATION-CORE-PASSPORT-METADATA-PASSPORT-METADATA-EXTERNAL-DOCUMENTATION-LINK-50F759", - ), - ], -) -def test_passport_metadata_public_validator_is_fail_fast_and_non_mutating( - contract_id: str, value: PassportMetadata | None, context: str -) -> None: - before = value.model_dump() if value is not None else None - with pytest.raises(DppValidationError, match=context): - validate_passport_metadata(value) - if value is not None: - assert value.model_dump() == before - - -def test_core_general_and_error_contracts(valid_core: DppCore) -> None: - with pytest.raises(DppValidationError, match="cannot be null"): - validate_dpp_core(None) - assert issubclass(DppValidationError, DppError) - validate_dpp_core(valid_core) - - -@pytest.mark.parametrize( - ("contract_id", "validator", "value"), - [ - pytest.param( - "VALIDATION-CORE-NAMEPLATE-NAMEPLATE-VALIDATOR-1DA7B8", - validate_nameplate, - Nameplate( - gtinCode="G1", - manufacturer=Organization(name="ACME", role=OrganizationRole.MANUFACTURER), - ), - id="VALIDATION-CORE-NAMEPLATE-NAMEPLATE-VALIDATOR-1DA7B8", - ), - pytest.param( - "VALIDATION-CORE-PASSPORT-METADATA-PASSPORT-METADATA-VALIDATOR-BF1A4A", - validate_passport_metadata, - PassportMetadata( - uniqueProductIdentifier=UUID(int=1), passportUpdateDates=(date(2024, 1, 1),) - ), - id="VALIDATION-CORE-PASSPORT-METADATA-PASSPORT-METADATA-VALIDATOR-BF1A4A", - ), - ], -) -def test_core_aggregate_leaf_validators_accept_valid_values_repeatedly( - contract_id: str, validator: Callable[[Any], None], value: Any -) -> None: - before = value.model_dump() - validator(value) - validator(value) - assert value.model_dump() == before - - -@pytest.mark.parametrize( - "contract_id", - [ - pytest.param( - "VALIDATION-CORE-GENERAL-CANNOT-VALIDATE-NULL-380A2C", - id="VALIDATION-CORE-GENERAL-CANNOT-VALIDATE-NULL-380A2C", - ), - pytest.param( - "VALIDATION-CORE-VALIDATION-EXCEPTION-VALIDATION-EXCEPTION-9B74AF", - id="VALIDATION-CORE-VALIDATION-EXCEPTION-VALIDATION-EXCEPTION-9B74AF", - ), - ], -) -def test_core_validation_null_and_error_category_contracts(contract_id: str) -> None: - with pytest.raises(DppValidationError, match="cannot be null"): - validate_dpp_core(None) diff --git a/tests/test_core_validation_parity.py b/tests/test_core_validation_parity.py new file mode 100644 index 0000000..d24afb2 --- /dev/null +++ b/tests/test_core_validation_parity.py @@ -0,0 +1,692 @@ +"""Source-backed core validation contract tests for final Java parity.""" + +from __future__ import annotations + +from dataclasses import FrozenInstanceError, dataclass +from datetime import UTC, datetime + +import pytest +from pydantic import ValidationError + +from dpp_sdk.core.model import ( + DataElementCollection, + DigitalProductPassport, + DppHeader, + Granularity, + MultiLanguageDataElement, + MultiLanguageValue, + MultiValuedDataElement, + RelatedResource, + SingleValuedDataElement, +) +from dpp_sdk.core.validation import ( + DataFormatValidator, + DppValidator, + EconomicOperatorIdentifierMethod, + FacilityIdentifierMethod, + IdentifierValidationContext, + IdentifierValidator, + ProductIdentifierMethod, + ValidationIssue, + ValidationResult, + ValidationSeverity, + ValidationStatus, +) + + +def header(**changes: object) -> DppHeader: + values: dict[str, object] = { + "digital_product_passport_id": "urn:example:dpp:1", + "unique_product_identifier": "free-form-product-id", + "granularity": Granularity.MODEL, + "dpp_schema_version": "2026", + "dpp_status": "future-open-status", + "last_updated": datetime(2026, 1, 1, tzinfo=UTC), + "economic_operator_id": "free-form-operator-id", + } + values.update(changes) + return DppHeader(**values) + + +def passport(*elements: object, **header_changes: object) -> DigitalProductPassport: + return DigitalProductPassport(dpp_header=header(**header_changes), data_elements=elements) + + +def normalized(result: ValidationResult) -> list[tuple[str, str, str]]: + return [(issue.rule, issue.path, issue.severity.value) for issue in result.issues] + + +@pytest.mark.parametrize( + ("field", "rule"), + [ + ("digital_product_passport_id", "EN18223_HEADER_DPP_ID_REQUIRED"), + ("unique_product_identifier", "EN18223_HEADER_PRODUCT_ID_REQUIRED"), + ("granularity", "EN18223_HEADER_GRANULARITY_REQUIRED"), + ("dpp_schema_version", "EN18223_HEADER_SCHEMA_VERSION_REQUIRED"), + ("dpp_status", "EN18223_HEADER_STATUS_REQUIRED"), + ("last_updated", "EN18223_HEADER_LAST_UPDATED_REQUIRED"), + ("economic_operator_id", "EN18223_HEADER_ECONOMIC_OPERATOR_ID_REQUIRED"), + ], +) +def test_required_header_presence_is_aggregated(field: str, rule: str) -> None: + result = DppValidator().validate(passport(**{field: None})) + wire_field = { + "digital_product_passport_id": "digitalProductPassportId", + "unique_product_identifier": "uniqueProductIdentifier", + "dpp_schema_version": "dppSchemaVersion", + "dpp_status": "dppStatus", + "last_updated": "lastUpdated", + "economic_operator_id": "economicOperatorId", + }.get(field, field) + assert normalized(result) == [(rule, f"header.{wire_field}", "ERROR")] + + +def test_header_warning_is_non_failing_and_ids_are_not_overrestricted() -> None: + result = DppValidator().validate(passport(digital_product_passport_id="plain-id")) + assert normalized(result) == [ + ("EN18223_HEADER_DPP_ID_URI_RECOMMENDED", "header.digitalProductPassportId", "WARNING") + ] + assert result.status is ValidationStatus.VALID + assert result.is_valid + + +def test_content_specification_values_are_nonblank_but_optional() -> None: + assert DppValidator().validate(passport(content_specification_ids=())).is_valid + result = DppValidator().validate(passport(content_specification_ids=("spec", " "))) + assert normalized(result) == [ + ("EN18223_HEADER_CONTENT_SPEC_ID_NONBLANK", "header.contentSpecificationIds", "ERROR") + ] + + +@pytest.mark.parametrize( + ("element", "expected"), + [ + ( + SingleValuedDataElement(element_id=" ", value="ok"), + ("EN18223_ELEMENT_ID_REQUIRED", "elements[0].elementId"), + ), + ( + DataElementCollection(element_id="e"), + ("EN18223_COLLECTION_NON_EMPTY", "elements[0].elements"), + ), + ( + SingleValuedDataElement(element_id="e"), + ("EN18223_SINGLE_VALUE_REQUIRED", "elements[0].value"), + ), + ( + MultiValuedDataElement(element_id="e"), + ("EN18223_MULTI_NON_EMPTY", "elements[0].elements"), + ), + ( + RelatedResource(element_id="e"), + ("EN18223_RELATED_RESOURCE_CONTENT_TYPE_REQUIRED", "elements[0].contentType"), + ), + ( + MultiLanguageDataElement(element_id="e"), + ("EN18223_MULTILANGUAGE_NON_EMPTY", "elements[0].values"), + ), + ], +) +def test_required_and_cardinality_rule_families(element: object, expected: tuple[str, str]) -> None: + result = DppValidator().validate(passport(element)) + assert (result.issues[0].rule, result.issues[0].path) == expected + + +def test_all_element_issue_order_and_nested_paths_are_deterministic() -> None: + values = MultiLanguageDataElement( + element_id="names", value=(MultiLanguageValue(value=None, language=None),) + ) + repeated = MultiValuedDataElement( + element_id="list", + value=( + SingleValuedDataElement(element_id="x", value=1), + SingleValuedDataElement(element_id="x", value="s"), + ), + ) + result = DppValidator().validate(passport(values, repeated, RelatedResource(element_id="r"))) + assert normalized(result) == [ + ("EN18223_ABSOLUTE_PATH_UNIQUE", "elements[1].elements[1].elementId", "ERROR"), + ("EN18223_MULTILANGUAGE_VALUE_REQUIRED", "elements[0].values[0].value", "ERROR"), + ("EN18223_MULTILANGUAGE_LANGUAGE_REQUIRED", "elements[0].values[0].language", "ERROR"), + ("EN18223_JSON_NATIVE_TYPE_MATCHES_XSD", "elements[1].elements[1]", "ERROR"), + ("EN18223_ELEMENT_ID_UNIQUE_WITHIN_LOCATION", "elements[1].elements[1].elementId", "ERROR"), + ("EN18223_RELATED_RESOURCE_CONTENT_TYPE_REQUIRED", "elements[2].contentType", "ERROR"), + ("EN18223_RELATED_RESOURCE_URL_REQUIRED", "elements[2].url", "ERROR"), + ] + + +def test_identifier_uniqueness_is_sibling_local_and_absolute_paths_are_unique() -> None: + tree = DataElementCollection( + element_id="a", elements=(SingleValuedDataElement(element_id="b", value="nested"),) + ) + flat = SingleValuedDataElement(element_id="a.b", value="flat") + siblings = ( + SingleValuedDataElement(element_id="same", value=1), + SingleValuedDataElement(element_id="same", value=2), + ) + result = DppValidator().validate( + passport(tree, flat, DataElementCollection(element_id="left", elements=siblings)) + ) + assert normalized(result) == [ + ("EN18223_ABSOLUTE_PATH_UNIQUE", "elements[1].elementId", "ERROR"), + ("EN18223_ABSOLUTE_PATH_UNIQUE", "elements[2].elements[1].elementId", "ERROR"), + ("EN18223_ELEMENT_ID_UNIQUE_WITHIN_LOCATION", "elements[2].elements[1].elementId", "ERROR"), + ] + + +def test_mixed_concrete_kinds_emit_rule_and_children_still_validate() -> None: + value = MultiValuedDataElement( + element_id="list", + value=( + SingleValuedDataElement(element_id="one", value=1), + DataElementCollection(element_id="two"), + ), + ) + result = DppValidator().validate(passport(value)) + assert normalized(result) == [ + ("EN18223_MULTI_HOMOGENEOUS_KIND", "elements[0].elements[1]", "ERROR"), + ("EN18223_COLLECTION_NON_EMPTY", "elements[0].elements[1].elements", "ERROR"), + ] + + +def test_malformed_python_shape_remains_a_construction_error() -> None: + with pytest.raises(ValidationError): + DppHeader.model_validate({"granularity": "not-a-granularity"}) + with pytest.raises(ValidationError): + DigitalProductPassport.model_validate({"elements": [{"objectType": "unknown"}]}) + + +def test_unknown_runtime_subtype_is_a_semantic_issue_only_after_structural_bypass() -> None: + @dataclass + class UnknownElement: + element_id: str = "custom" + + aggregate = DigitalProductPassport.model_construct( + dpp_header=header(), data_elements=(UnknownElement(),) + ) + result = DppValidator().validate(aggregate) # type: ignore[arg-type] + assert normalized(result) == [ + ("EN18223_OBJECT_TYPE_MATCHES_RUNTIME_KIND", "elements[0]", "ERROR") + ] + + +def test_null_passport_and_issue_result_contracts() -> None: + result = DppValidator().validate(None) + assert result.status is ValidationStatus.INVALID + assert normalized(result) == [("DPP_REQUIRED", "dpp", "ERROR")] + assert not result.is_valid + issue = ValidationIssue("x", "X", "message") + assert issue.severity is ValidationSeverity.ERROR + assert ValidationIssue.Severity.WARNING is ValidationSeverity.WARNING + assert ValidationResult.of([issue]).issues == (issue,) + with pytest.raises(AttributeError): + result.issues.append(issue) # type: ignore[attr-defined] + with pytest.raises(FrozenInstanceError): + result._status = ValidationStatus.VALID # type: ignore[misc] + + +def test_format_validation_covers_datatype_vocabulary_values_and_references() -> None: + items = ( + SingleValuedDataElement(element_id="ok", value="hi", value_data_type="xsd:string"), + SingleValuedDataElement(element_id="bad-type", value="opaque", value_data_type="xsd:token"), + SingleValuedDataElement(element_id="bad-value", value="1e2", value_data_type="xsd:decimal"), + SingleValuedDataElement( + element_id="bad-uri", value="relative", value_data_type="xsd:anyURI" + ), + MultiValuedDataElement( + element_id="mixed-types", + value_data_type="xsd:string", + value=( + SingleValuedDataElement(element_id="v", value="ok", value_data_type="xsd:integer"), + ), + ), + ) + result = DataFormatValidator().validate(passport(*items)) + assert normalized(result) == [ + ("EN18223_VALUE_DATATYPE_SUPPORTED", "elements[1].valueDataType", "ERROR"), + ("EN18223_VALUE_MATCHES_DATATYPE", "elements[2].value", "ERROR"), + ("EN18223_VALUE_MATCHES_DATATYPE", "elements[3].value", "ERROR"), + ("EN18223_MULTI_HOMOGENEOUS_DATATYPE", "elements[4].elements[0].valueDataType", "ERROR"), + ("EN18223_VALUE_MATCHES_DATATYPE", "elements[4].elements[0].value", "ERROR"), + ] + + +@pytest.mark.parametrize( + ("value_type", "value", "expected_rule"), + [ + ("xsd:madeUpType", "opaque", "EN18223_VALUE_DATATYPE_SUPPORTED"), + ("xsd:boolean", "true", "EN18223_VALUE_MATCHES_DATATYPE"), + ("xsd:integer", 1.5, "EN18223_VALUE_MATCHES_DATATYPE"), + ("xsd:date", "2026-02-30", "EN18223_VALUE_MATCHES_DATATYPE"), + ("xsd:dateTime", "2026-01-01T12:00:00+00:00", "EN18223_VALUE_MATCHES_DATATYPE"), + ("xsd:base64Binary", "not base64!", "EN18223_VALUE_MATCHES_DATATYPE"), + ("xsd:positiveInteger", 0, "EN18223_VALUE_MATCHES_DATATYPE"), + ], +) +def test_datatype_failure_families_have_canonical_rule_path_and_severity( + value_type: str, value: object, expected_rule: str +) -> None: + result = DataFormatValidator().validate( + passport(SingleValuedDataElement(element_id="v", value=value, value_data_type=value_type)) + ) + path = ( + "elements[0].value" + if expected_rule == "EN18223_VALUE_MATCHES_DATATYPE" + else "elements[0].valueDataType" + ) + assert normalized(result) == [(expected_rule, path, "ERROR")] + + +def test_all_java_xsd_datatype_categories_and_integer_boundaries_are_accounted_for() -> None: + type_values = { + "anySimpleType": {"opaque": True}, + "duration": {"opaque": True}, + "time": {"opaque": True}, + "gYearMonth": {"opaque": True}, + "gYear": {"opaque": True}, + "gMonthDay": {"opaque": True}, + "gDay": {"opaque": True}, + "gMonth": {"opaque": True}, + "hexBinary": {"opaque": True}, + "integer": 1, + "nonPositiveInteger": 0, + "negativeInteger": -1, + "long": 1, + "int": 1, + "short": 1, + "byte": 1, + "nonNegativeInteger": 0, + "unsignedLong": 1, + "unsignedInt": 1, + "unsignedShort": 1, + "unsignedByte": 1, + "positiveInteger": 1, + } + elements = tuple( + SingleValuedDataElement( + element_id=f"allowed-{kind}", value=value, value_data_type=f"xsd:{kind}" + ) + for kind, value in type_values.items() + ) + assert DataFormatValidator().validate(passport(*elements)).is_valid + + prohibited = ( + "ENTITIES", + "IDREFS", + "NMTOKENS", + "normalizedString", + "token", + "language", + "NMTOKEN", + "Name", + "NCName", + "ID", + "IDREF", + "ENTITY", + "NOTATION", + "QName", + ) + rejected = tuple( + SingleValuedDataElement( + element_id=f"prohibited-{kind}", value="opaque", value_data_type=f"xsd:{kind}" + ) + for kind in prohibited + ) + assert len(DataFormatValidator().validate(passport(*rejected)).issues) == len(prohibited) + assert normalized( + DataFormatValidator().validate( + passport( + SingleValuedDataElement( + element_id="min", value=255, value_data_type="xsd:unsignedByte" + ), + SingleValuedDataElement( + element_id="over", value=256, value_data_type="xsd:unsignedByte" + ), + ) + ) + ) == [("EN18223_VALUE_MATCHES_DATATYPE", "elements[1].value", "ERROR")] + + +def test_multivalue_declared_datatypes_conflict_and_untyped_siblings_must_match() -> None: + conflict = MultiValuedDataElement( + element_id="declared", + value_data_type="xsd:string", + value=( + SingleValuedDataElement(element_id="child", value=1, value_data_type="xsd:integer"), + ), + ) + mismatch = MultiValuedDataElement( + element_id="inferred", + value=( + SingleValuedDataElement(element_id="first", value=1, value_data_type="xsd:integer"), + SingleValuedDataElement(element_id="second", value=1.0, value_data_type="xsd:double"), + ), + ) + assert normalized(DataFormatValidator().validate(passport(conflict, mismatch))) == [ + ("EN18223_MULTI_HOMOGENEOUS_DATATYPE", "elements[0].elements[0].valueDataType", "ERROR"), + ("EN18223_MULTI_HOMOGENEOUS_DATATYPE", "elements[1].elements[1].valueDataType", "ERROR"), + ] + + +def test_format_validation_resource_media_language_and_no_semantic_duplicates() -> None: + resource = RelatedResource( + element_id="r", url="relative", content_type="application/", language="en" + ) + core = passport( + resource, + MultiLanguageDataElement( + element_id="m", value=(MultiLanguageValue(value="name", language="zz-ZZ"),) + ), + ) + assert normalized(DataFormatValidator().validate(core)) == [ + ("EN18223_RELATED_RESOURCE_URL_RFC3986", "elements[0].url", "ERROR"), + ("EN18223_RELATED_RESOURCE_CONTENT_TYPE_SYNTAX", "elements[0].contentType", "ERROR"), + ("EN18223_RELATED_RESOURCE_LANGUAGE_TAG", "elements[0].language", "ERROR"), + ("EN18223_MULTILANGUAGE_LANGUAGE_TAG", "elements[1].values[0].language", "ERROR"), + ] + assert ( + DataFormatValidator() + .validate(passport(SingleValuedDataElement(element_id="missing"))) + .is_valid + ) + assert DppValidator().validate(core).is_valid + + +def test_identifier_profiles_are_explicit_and_granularity_checked() -> None: + context = IdentifierValidationContext( + ProductIdentifierMethod.GS1_DIGITAL_LINK, + EconomicOperatorIdentifierMethod.GS1_GLN, + ) + valid = header( + unique_product_identifier="https://id.gs1.org/01/09506000134352/10/LOT-1", + granularity=Granularity.BATCH, + economic_operator_id="4171234567890128", + ) + assert IdentifierValidator().validate(valid, context).is_valid + invalid = valid.with_updates(granularity=Granularity.MODEL) + result = IdentifierValidator().validate(invalid, context) + assert normalized(result) == [ + ("IDENTIFIER_GRANULARITY_MISMATCH", "header.granularity", "ERROR") + ] + bad_gtin = valid.with_updates( + unique_product_identifier="https://id.gs1.org/01/09506000134353/10/LOT-1" + ) + assert normalized(IdentifierValidator().validate(bad_gtin, context))[0] == ( + "GS1_DIGITAL_LINK_KEY_INVALID", + "header.uniqueProductIdentifier", + "ERROR", + ) + + +@pytest.mark.parametrize( + ("operator", "facility", "method", "expected"), + [ + ("4171234567890129", None, None, ("GS1_GLN_INVALID", "header.economicOperatorId")), + ("4141234567890128", None, None, ("GS1_GLN_ROLE_MISMATCH", "header.economicOperatorId")), + ( + "4171234567890128", + "bad", + FacilityIdentifierMethod.GS1_GLN, + ("GS1_GLN_INVALID", "header.facilityId"), + ), + ( + "4171234567890128", + "4171234567890128", + FacilityIdentifierMethod.GS1_GLN, + ("GS1_GLN_ROLE_MISMATCH", "header.facilityId"), + ), + ], +) +def test_gln_syntax_and_role_rule_family( + operator: str, + facility: str | None, + method: FacilityIdentifierMethod | None, + expected: tuple[str, str], +) -> None: + value = header( + unique_product_identifier="https://example.test/item/1", + granularity=Granularity.ITEM, + economic_operator_id=operator, + facility_id=facility, + ) + context = IdentifierValidationContext( + ProductIdentifierMethod.IDENTIFICATION_LINK, + EconomicOperatorIdentifierMethod.GS1_GLN, + method, + ) + result = IdentifierValidator().validate(value, context) + assert normalized(result) == [(expected[0], expected[1], "ERROR")] + + +@pytest.mark.parametrize( + ("identifier", "granularity", "expected_rule", "status"), + [ + ( + "https://example.test/product?.1P", + Granularity.MODEL, + "IDENTIFICATION_LINK_STRUCTURED_SYNTAX_INVALID", + ValidationStatus.INVALID, + ), + ( + "https://example.test/product?.1T=LOT", + Granularity.BATCH, + "IDENTIFICATION_LINK_REQUIRED_IDENTIFIER_MISSING", + ValidationStatus.INVALID, + ), + ( + "https://example.test/product?.2Q=VALUE", + Granularity.MODEL, + "IDENTIFICATION_LINK_DI_UNSUPPORTED", + ValidationStatus.UNSUPPORTED, + ), + ], +) +def test_identification_link_rule_families( + identifier: str, granularity: Granularity, expected_rule: str, status: ValidationStatus +) -> None: + value = header( + unique_product_identifier=identifier, + granularity=granularity, + economic_operator_id="4171234567890128", + ) + context = IdentifierValidationContext( + ProductIdentifierMethod.IDENTIFICATION_LINK, + EconomicOperatorIdentifierMethod.GS1_GLN, + ) + result = IdentifierValidator().validate(value, context) + assert normalized(result) == [(expected_rule, "header.uniqueProductIdentifier", "ERROR")] + assert result.status is status + + +def test_identifier_unsupported_plus_invalid_is_invalid_and_warnings_do_not_fail() -> None: + context = IdentifierValidationContext( + ProductIdentifierMethod.UNSUPPORTED, + EconomicOperatorIdentifierMethod.GS1_GLN, + ) + mixed = IdentifierValidator().validate(header(economic_operator_id="bad"), context) + assert mixed.status is ValidationStatus.INVALID + warning_and_error = DppValidator().validate( + passport(SingleValuedDataElement(element_id="e"), digital_product_passport_id="relative") + ) + assert warning_and_error.status is ValidationStatus.INVALID + assert normalized(warning_and_error) == [ + ("EN18223_HEADER_DPP_ID_URI_RECOMMENDED", "header.digitalProductPassportId", "WARNING"), + ("EN18223_SINGLE_VALUE_REQUIRED", "elements[0].value", "ERROR"), + ] + + +def test_identifier_api_missing_inputs_and_granularity_required() -> None: + validator = IdentifierValidator() + assert normalized(validator.validate(None, None)) == [ + ("IDENTIFIER_HEADER_REQUIRED", "header", "ERROR") + ] + assert normalized(validator.validate(header(), None)) == [ + ("IDENTIFIER_VALIDATION_CONTEXT_REQUIRED", "context", "ERROR") + ] + context = IdentifierValidationContext( + ProductIdentifierMethod.GS1_DIGITAL_LINK, EconomicOperatorIdentifierMethod.GS1_GLN + ) + missing_granularity = header( + unique_product_identifier="https://example.test/01/09506000134352", + granularity=None, + economic_operator_id="4171234567890128", + ) + result = validator.validate(missing_granularity, context) + assert normalized(result) == [ + ("IDENTIFIER_GRANULARITY_REQUIRED", "header.granularity", "ERROR") + ] + + +def test_identification_link_and_unsupported_profile_semantics() -> None: + valid = header( + unique_product_identifier="https://example.test/p?.P=MODEL&.T=LOT", + granularity=Granularity.BATCH, + economic_operator_id="4171234567890128", + ) + context = IdentifierValidationContext( + ProductIdentifierMethod.IDENTIFICATION_LINK, + EconomicOperatorIdentifierMethod.GS1_GLN, + ) + assert IdentifierValidator().validate(valid, context).is_valid + unsupported = IdentifierValidationContext( + ProductIdentifierMethod.UNSUPPORTED, + EconomicOperatorIdentifierMethod.UNSUPPORTED, + ) + result = IdentifierValidator().validate(valid, unsupported) + assert result.status is ValidationStatus.UNSUPPORTED + assert {issue.rule for issue in result.issues} == {"IDENTIFIER_METHOD_UNSUPPORTED"} + + +def test_identifier_facility_requires_explicit_profile_and_gln_role() -> None: + valid = header( + unique_product_identifier="https://example.test/item/one", + granularity=Granularity.ITEM, + economic_operator_id="4171234567890128", + facility_id="41412345678909", + ) + context = IdentifierValidationContext( + ProductIdentifierMethod.IDENTIFICATION_LINK, + EconomicOperatorIdentifierMethod.GS1_GLN, + ) + result = IdentifierValidator().validate(valid, context) + assert normalized(result) == [("IDENTIFIER_METHOD_UNSUPPORTED", "header.facilityId", "ERROR")] + facility_context = IdentifierValidationContext( + ProductIdentifierMethod.IDENTIFICATION_LINK, + EconomicOperatorIdentifierMethod.GS1_GLN, + FacilityIdentifierMethod.GS1_GLN, + ) + assert not IdentifierValidator().validate(valid, facility_context).is_valid + + +@pytest.mark.parametrize( + ("identifier", "granularity", "rule", "status"), + [ + ( + "http://example.test/01/09506000134352", + Granularity.MODEL, + "GS1_DIGITAL_LINK_URL_INVALID", + ValidationStatus.INVALID, + ), + ( + "https://example.test/01/09506000134353", + Granularity.MODEL, + "GS1_DIGITAL_LINK_KEY_INVALID", + ValidationStatus.INVALID, + ), + ( + "https://example.test/8013/model", + Granularity.MODEL, + "GS1_DIGITAL_LINK_KEY_UNSUPPORTED", + ValidationStatus.UNSUPPORTED, + ), + ( + "https://example.test/01/09506000134352/10/A/10/B", + Granularity.BATCH, + "GS1_DIGITAL_LINK_QUALIFIER_INVALID", + ValidationStatus.INVALID, + ), + ( + "https://example.test/01/09506000134352?17=260101", + Granularity.MODEL, + "GS1_DIGITAL_LINK_QUERY_UNSUPPORTED", + ValidationStatus.UNSUPPORTED, + ), + ], +) +def test_gs1_identifier_profile_java_rule_samples( + identifier: str, granularity: Granularity, rule: str, status: ValidationStatus +) -> None: + value = header( + unique_product_identifier=identifier, + granularity=granularity, + economic_operator_id="4171234567890128", + ) + context = IdentifierValidationContext( + ProductIdentifierMethod.GS1_DIGITAL_LINK, EconomicOperatorIdentifierMethod.GS1_GLN + ) + result = IdentifierValidator().validate(value, context) + assert result.status is status + assert result.issues[0].rule == rule + + +def test_gs1_xchar_encoding_and_identification_link_boundaries() -> None: + context = IdentifierValidationContext( + ProductIdentifierMethod.GS1_DIGITAL_LINK, EconomicOperatorIdentifierMethod.GS1_GLN + ) + for qualifier in ("LOT%2F1",): + value = header( + unique_product_identifier=f"https://example.test/01/09506000134352/10/{qualifier}", + granularity=Granularity.BATCH, + economic_operator_id="4171234567890128", + ) + assert IdentifierValidator().validate(value, context).is_valid + for qualifier in ("LOT%41", "LOT&1"): + value = header( + unique_product_identifier=f"https://example.test/01/09506000134352/10/{qualifier}", + granularity=Granularity.BATCH, + economic_operator_id="4171234567890128", + ) + assert ( + IdentifierValidator().validate(value, context).issues[0].rule + == "GS1_DIGITAL_LINK_QUALIFIER_INVALID" + ) + + identification = IdentifierValidationContext( + ProductIdentifierMethod.IDENTIFICATION_LINK, EconomicOperatorIdentifierMethod.GS1_GLN + ) + for text in ("example.test:8443/item/one", "www.example.test/item/one"): + value = header( + unique_product_identifier=text, + granularity=Granularity.ITEM, + economic_operator_id="4171234567890128", + ) + assert IdentifierValidator().validate(value, identification).is_valid + for text in ("https://xn--name.test/item", "not a url", "https://example.test/item%2Fpart"): + value = header( + unique_product_identifier=text, + granularity=Granularity.ITEM, + economic_operator_id="4171234567890128", + ) + assert ( + IdentifierValidator().validate(value, identification).issues[0].rule + == "IDENTIFICATION_LINK_URL_INVALID" + ) + + +def test_identifier_profiles_keep_operator_and_facility_roles_distinct() -> None: + value = header( + unique_product_identifier="https://example.test/item/one", + granularity=Granularity.ITEM, + economic_operator_id="4141234567890128", + facility_id="4171234567890128", + ) + context = IdentifierValidationContext( + ProductIdentifierMethod.IDENTIFICATION_LINK, + EconomicOperatorIdentifierMethod.GS1_GLN, + FacilityIdentifierMethod.GS1_GLN, + ) + result = IdentifierValidator().validate(value, context) + assert normalized(result) == [ + ("GS1_GLN_ROLE_MISMATCH", "header.economicOperatorId", "ERROR"), + ("GS1_GLN_ROLE_MISMATCH", "header.facilityId", "ERROR"), + ] diff --git a/tests/test_documentation.py b/tests/test_documentation.py index 3dec240..e90df14 100644 --- a/tests/test_documentation.py +++ b/tests/test_documentation.py @@ -14,12 +14,12 @@ MARKDOWN_FILES = ( "README.md", "RELEASING.md", - "docs/overview.md", - "docs/usage.md", + "CONTRIBUTING.md", "docs/model-guide.md", - "docs/validation-guide.md", - "docs/validation-rules.md", - "docs/diagram-evidence.md", + "docs/clients-guide.md", + "docs/extending.md", + "docs/conformance.md", + "docs/dpp4fun-fields.md", "src/dpp_sdk/core/README.md", "src/dpp_sdk/dpp4fun/README.md", "src/dpp_sdk/clients/README.md", @@ -28,55 +28,47 @@ ) FAMILIES = { "README.md": ( - "## Start here", - "## Architecture at a glance", - "## Prerequisites", "## Install", - "## Documentation and next steps", + "## Create, save, and register a passport", + "## How the SDK is organized", + "## Handle errors", + "## Documentation", + "## Standards and limitations", + "## Versioning", ), - "docs/overview.md": ( - "## Purpose and scope", - "## Architecture and package boundaries", - "## Supported concepts", - "## Intentional exclusions and limitations", - "## Reading order and next steps", - ), - "docs/usage.md": ("## Purpose and scope", "## Installation", "## Errors", "Next:"), "docs/model-guide.md": ( - "## Purpose and scope", - "## Reusable core", - "## DPP4Fun models", - "## Immutable updates and validation boundary", - "## Limitations and next steps", - ), - "docs/validation-guide.md": ( - "## Purpose and scope", - "## Construction versus semantic validation", - "## Codec input, output, and failures", - "## Errors, limitations, and next steps", + "## Overview", + "## Build a core passport", + "## Build a DPP4Fun passport", + "## Read fields", + "## Edit", + "## Validate", + "## Convert to and from JSON", ), - "docs/validation-rules.md": ( - "## Purpose and scope", + "docs/conformance.md": ( + "## Source alignment and source conflicts", "## Core rules", "## DPP4Fun rules", - "## Codec boundary, limitations, and next steps", ), - "src/dpp_sdk/clients/README.md": ( - "## Purpose and scope", - "## Shared response envelope, DTOs, and errors", - "## Repository operation reference", - "## Registry operation reference", - "## Related documents and next steps", + "docs/clients-guide.md": ( + "## Public operations", + "## Envelopes and errors", + "## Pagination and registry metadata", + "## Decode a response into a typed passport", + ), + "CONTRIBUTING.md": ( + "## Set up the development environment", + "## Checks", + "## Architecture diagrams", + "## Mock-service verification", ), } +EXECUTED_GUIDES = ("docs/model-guide.md", "docs/extending.md") REPOSITORY_OPERATIONS = ( - "health_check", "create_dpp", "read_dpp_by_id", "read_dpp_by_product_id", - "read_compressed_dpp_by_id", "read_dpp_version_by_id_and_date", - "read_dpp_version_by_product_id_and_date", "read_dpp_ids_by_product_ids", "update_dpp_by_id", "read_data_element", @@ -90,13 +82,8 @@ "python-dpp4fun-model", ) COMMAND_DOCUMENTS = ( - "README.md", + "CONTRIBUTING.md", "RELEASING.md", - "docs/usage.md", - "docs/diagram-evidence.md", - "src/dpp_sdk/core/README.md", - "src/dpp_sdk/dpp4fun/README.md", - "src/dpp_sdk/clients/README.md", "examples/mock-services-demo/README.md", ) @@ -182,15 +169,17 @@ def _validate_operation_section(section: str, operation: str) -> None: def _validate_client_operations(text: str) -> None: for operation in REPOSITORY_OPERATIONS: - _validate_operation_section(_operation_section(text, operation), operation) - _validate_operation_section( - _operation_section(text, "health_check", 1), "registry health_check" - ) - _validate_operation_section( - _operation_section(text, "post_new_dpp_to_registry"), "post_new_dpp_to_registry" - ) - assert text.count("### health_check") == 2 - assert "deprecated unversioned compatibility route" in text + assert f"`{operation}" in text or f"| `{operation}" in text, operation + assert "post_new_dpp_to_registry" in text + for obsolete in ( + "health_check", + "read_compressed_dpp_by_id", + "read_dpp_version_by_product_id_and_date", + "DppCodec", + "UpdateDataElementRequest", + "deprecated unversioned compatibility route", + ): + assert obsolete not in text def _validate_diagram_pairs(directory: Path) -> None: @@ -225,7 +214,7 @@ def _validate_root_command_convention(overrides: dict[str, str] | None = None) - operations = _read("examples/mock-services-demo/ADVANCED_OPERATIONS.md") assert "from `examples/mock-services-demo`" not in demo + operations assert "docker compose --env-file $envFile -f $composeFile -p $project up -d --wait" in demo - assert "Optional PowerShell convenience wrapper" in demo + assert "[ADVANCED_OPERATIONS.md](ADVANCED_OPERATIONS.md)" in demo def test_documentation_structure_links_examples_payloads_and_diagrams() -> None: @@ -240,31 +229,49 @@ def test_documentation_structure_links_examples_payloads_and_diagrams() -> None: text = _read(relative) for heading in headings: assert heading in text, f"{relative} missing {heading}" - _validate_client_operations(_read("src/dpp_sdk/clients/README.md")) + _validate_client_operations(_read("docs/clients-guide.md")) _validate_diagram_pairs(ROOT / "docs/architecture") _validate_root_command_convention() +def test_package_manifest_includes_maintained_diagram_sources_and_previews() -> None: + import tomllib + + manifest = tomllib.loads(_read("pyproject.toml")) + included = manifest["tool"]["hatch"]["build"]["targets"]["sdist"]["include"] + + assert "docs/architecture/*.mmd" in included + assert "docs/architecture/*.svg" in included + _validate_diagram_pairs(ROOT / "docs" / "architecture") + + +def test_client_guide_example_uses_the_flat_canonical_passport_shape() -> None: + guide = _read("docs/clients-guide.md") + assert '"digitalProductPassportId"' in guide + assert '"elements"' in guide + assert '"dpp_header"' not in guide + + def test_demo_readme_leads_with_small_consumer_sdk_use() -> None: demo = _read("examples/mock-services-demo/README.md") assert "-m dpp_mock_services_demo sdk" in demo - assert "offline example does not load a service profile" in demo - assert "SDK-01 through SDK-17" not in demo + assert demo.index("## Quick start") < demo.index("## Optional connected demo") -def test_root_readme_keeps_the_optional_mock_service_path_minimal() -> None: +def test_root_readme_renders_on_pypi_and_starts_services_without_a_checkout() -> None: readme = _read("README.md") - assert "## Optional Mock-services demo" in readme - assert "Docker Compose" in readme - assert "docker compose --env-file $envFile -f $composeFile -p $project up -d --wait" in readme - assert "-m dpp_mock_services_demo demo --env-file $envFile" in readme - assert "docker compose --env-file $envFile -f $composeFile -p $project down -v" in readme - assert "## Linux/macOS" in readme - assert "ADVANCED_OPERATIONS.md" in readme - assert "Quick Mock-service checks" not in readme - assert "test_integration_live.py" not in readme + # PyPI cannot resolve repository-relative paths, so every link and image must be absolute. + for target in re.findall(r"\]\(([^)]+)\)", readme) + re.findall(r'src="([^"]+)"', readme): + assert "://" in target, f"relative link breaks on PyPI: {target}" + raw = "https://raw.githubusercontent.com/CIR4FUN-EU/python-dpp-sdk/main/" + assert f"{raw}examples/mock-services-demo/compose.yaml" in readme + assert (ROOT / "examples/mock-services-demo/compose.yaml").is_file() + assert "pip install dpp-sdk" in readme + assert "docker compose up -d --wait" in readme + assert "docker compose down" in readme + assert "--env-file" not in readme def test_demo_guide_offers_native_compose_commands() -> None: @@ -272,16 +279,23 @@ def test_demo_guide_offers_native_compose_commands() -> None: assert "docker compose --env-file $envFile -f $composeFile -p $project pull" in demo assert "docker compose --env-file $envFile -f $composeFile -p $project up -d --wait" in demo - assert "Optional PowerShell convenience wrapper" in demo + assert "[ADVANCED_OPERATIONS.md](ADVANCED_OPERATIONS.md)" in demo def test_demo_setup_defines_compose_variables_used_by_operations_reference() -> None: demo = _read("examples/mock-services-demo/README.md") operations = _read("examples/mock-services-demo/ADVANCED_OPERATIONS.md") + environment = _read("examples/mock-services-demo/.env.example") assert '$composeFile = Join-Path $demoDir "compose.yaml"' in demo - assert "$composeFile" in operations - assert "$compose_file" in operations + # The operations page defines its own variables before the first command that uses them. + setup = operations.index("## Set up the variables first") + assert operations.index('$composeFile = Join-Path $demoDir "compose.yaml"') > setup + assert operations.index('compose_file="$demo_dir/compose.yaml"') > setup + assert operations.index("$composeFile", setup) < operations.index("## Alternate ports") + assert "does not guarantee that its images are publicly pullable" in operations + assert "DPP_REPO_IMAGE=ghcr.io/cir4fun-eu/dpp-repo-api:" in environment + assert "DPP_REGISTRY_IMAGE=ghcr.io/cir4fun-eu/dpp-registry-api:" in environment def test_documentation_corruption_checks_fail_on_temporary_inputs(tmp_path: Path) -> None: @@ -291,14 +305,9 @@ def test_documentation_corruption_checks_fail_on_temporary_inputs(tmp_path: Path for heading in FAMILIES["docs/model-guide.md"]: assert heading in temporary.read_text(encoding="utf-8") - client = _read("src/dpp_sdk/clients/README.md") + client = _read("docs/clients-guide.md") with pytest.raises(AssertionError): - _validate_client_operations(client.replace("### delete_dpp_by_id", "### removed_operation")) - missing_null_behavior = ( - _operation_section(client, "read_dpp_by_id").replace("null", "").replace("Null", "") - ) - with pytest.raises(AssertionError): - _validate_operation_section(missing_null_behavior, "read_dpp_by_id") + _validate_client_operations(client.replace("read_dpp_by_id", "removed_operation")) with pytest.raises(json.JSONDecodeError): json.loads("{") with pytest.raises(AssertionError): @@ -311,27 +320,27 @@ def test_documentation_corruption_checks_fail_on_temporary_inputs(tmp_path: Path assert not re.search(r"[A-Za-z]:\\\\", "C:\\\\absolute\\\\path") with pytest.raises(AssertionError): _validate_root_command_convention( - {"README.md": _read("README.md").replace("Run from:", "")} + {"CONTRIBUTING.md": _read("CONTRIBUTING.md").replace("Run from:", "")} ) def test_installation_and_demo_commands_keep_their_working_directory_contracts() -> None: - readme = _read("README.md") - usage = _read("docs/usage.md") + contributing = _read("CONTRIBUTING.md") release = _read("RELEASING.md") demo = _read("examples/mock-services-demo/README.md") operations = _read("examples/mock-services-demo/ADVANCED_OPERATIONS.md") - for text in (readme, usage): - assert re.search(r"\*\*Run from:\*\* any\s+directory", text) - assert "directory-independent" in text - assert "**Run from:** the Python repository root" in readme + assert "folder that contains `pyproject.toml`" in contributing + assert "**Run from:** the repository root" in contributing assert "**Run from:** the Python repository root" in release + assert "git pull https://github.com/CIR4FUN-EU/python-dpp-sdk.git main" in release + assert "git push https://github.com/CIR4FUN-EU/python-dpp-sdk.git vX.Y.Z" in release assert "from `examples/mock-services-demo`" not in demo + operations assert '$project = "dpp-mock-services-demo-local"' in demo - assert 'DPP_REPO_BASE_URL = "http://localhost:18080"' in operations - assert "DPP_REPO_BASE_URL=http://localhost:18080" in operations - assert "down -v" in demo + assert 'DPP_REPO_BASE_URL = "http://localhost:28080"' in operations + assert "DPP_REPO_BASE_URL=http://localhost:28080" in operations + assert "18080" in demo and "8080 and 8081" in demo + assert "down" in demo assert "project-scoped" in demo @@ -364,10 +373,10 @@ def test_maintained_sdk_only_references_match_the_current_scenario_contract() -> scenario_path = ROOT / "examples/mock-services-demo" / "src/dpp_mock_services_demo" scenarios = (scenario_path / "sdk_scenarios.py").read_text(encoding="utf-8") - for text in (changelog, scenarios): - assert "SDK-01 through SDK-17" in text - assert "SDK-01 through SDK-15" not in text - assert "SDK-01" in demo and "SDK-07" in demo + assert "SDK-01 through SDK-17" in changelog + assert '"SDK-12"' in scenarios + assert '"SDK-13"' not in scenarios + assert "[sdk_scenarios.py](src/dpp_mock_services_demo/sdk_scenarios.py)" in demo def test_demo_report_commands_use_temporary_output_instead_of_the_worktree() -> None: @@ -382,36 +391,33 @@ def test_demo_report_commands_use_temporary_output_instead_of_the_worktree() -> def test_demo_readme_keeps_cleanup_with_the_live_lifecycle_and_links_operations() -> None: demo = _read("examples/mock-services-demo/README.md") - assert "## Full SDK and live service test suites" in demo - assert "pytest .\\tests --run-mock-services --force-sugar" in demo + assert "## Optional connected demo" in demo assert "[ADVANCED_OPERATIONS.md](ADVANCED_OPERATIONS.md)" in demo - assert "manage-mock-services.ps1" in demo - assert "-Action Start" in demo - assert "down -v" in demo + assert "manage-mock-services.ps1" in _read("examples/mock-services-demo/ADVANCED_OPERATIONS.md") + assert "down" in demo def test_full_demo_verification_uses_clean_wheels_and_runs_live_contracts_once() -> None: - demo = _read("examples/mock-services-demo/README.md") operations = _read("examples/mock-services-demo/ADVANCED_OPERATIONS.md") - assert "-m pytest .\\tests --run-mock-services --force-sugar" in demo - assert "-m pytest -c .\\examples\\mock-services-demo\\pyproject.toml" in demo - assert '$verifyVenv = Join-Path (Resolve-Path .).Path ".verify-venv"' in demo - assert "dpp_mock_services_demo.maintainer verify" in demo - assert "--summary" in demo + assert "-m pytest .\\tests --run-mock-services --force-sugar" in operations + assert "-m pytest -c .\\examples\\mock-services-demo\\pyproject.toml" in operations + assert '$verifyVenv = Join-Path (Resolve-Path .).Path ".verify-venv"' in operations + assert "dpp_mock_services_demo.maintainer verify" in operations + assert "--summary" in operations assert "## Choose a verification level" in operations - assert "22 live scenarios once" in operations + assert "19 live scenarios once" in operations def test_demo_guides_keep_detailed_verification_outside_the_root_readme() -> None: demo = _read("examples/mock-services-demo/README.md") operations = _read("examples/mock-services-demo/ADVANCED_OPERATIONS.md") - assert "Optional maintainer verification" in demo - assert "dpp_mock_services_demo.maintainer" in demo + assert "Optional maintainer verification" in operations + assert "dpp_mock_services_demo.maintainer" in operations for text in (demo, operations): assert "demo" in text - assert "verify" in text + assert "verify" in operations def test_demo_readme_uses_a_short_consumer_path() -> None: @@ -419,11 +425,14 @@ def test_demo_readme_uses_a_short_consumer_path() -> None: assert "## Quick start" in demo assert "## Optional connected demo" in demo - assert "## Linux/macOS" in demo + assert "### PowerShell" in demo + assert "### Bash (Linux/macOS)" in demo assert 'demo_dir="$(cd ./examples/mock-services-demo && pwd)"' in demo - assert ".venv/bin/python -m dpp_mock_services_demo sdk" in demo - assert 'DPP_DEMO_ENV_FILE="$env_file"' in demo - assert "## Optional maintainer verification" in demo + assert "python -m dpp_mock_services_demo sdk" in demo + assert 'docker compose --env-file "$env_file"' in demo + assert "## Optional maintainer verification" in _read( + "examples/mock-services-demo/ADVANCED_OPERATIONS.md" + ) assert "[sdk_scenarios.py](src/dpp_mock_services_demo/sdk_scenarios.py)" in demo assert "[integration_scenarios.py](src/dpp_mock_services_demo/integration_scenarios.py)" in demo @@ -431,8 +440,10 @@ def test_demo_readme_uses_a_short_consumer_path() -> None: def test_demo_readme_identifies_the_optional_wrapper_boundary() -> None: demo = _read("examples/mock-services-demo/README.md") - assert "does not depend on PowerShell script execution policy" in demo - assert "Optional PowerShell convenience wrapper" in demo + assert "manage-mock-services.ps1" not in demo + assert "Optional PowerShell lifecycle helper" in _read( + "examples/mock-services-demo/ADVANCED_OPERATIONS.md" + ) assert "[ADVANCED_OPERATIONS.md](ADVANCED_OPERATIONS.md)" in demo @@ -446,6 +457,35 @@ def test_demo_consumer_headings_remain_linkable() -> None: assert { "#quick-start", "#optional-connected-demo", - "#optional-maintainer-verification", - "#full-sdk-and-live-service-test-suites", } <= anchors + + +def test_consumer_documentation_does_not_repeat_version_numbers() -> None: + """Versions live in the bump checklist files only; docs must not need edits on a bump.""" + package = (ROOT / "src/dpp_sdk/__init__.py").read_text(encoding="utf-8") + match = re.search(r'__version__ = "([^"]+)"', package) + assert match + for relative in MARKDOWN_FILES: + if relative in {"RELEASING.md", "CONTRIBUTING.md"}: # name versioned service profiles + continue + text = _read(relative) + assert match.group(1) not in text, f"{relative} repeats the package version" + assert not re.search(r"ghcr\.io/\S+:\d", text), f"{relative} repeats an image tag" + + +@pytest.mark.parametrize("relative", EXECUTED_GUIDES) +def test_guide_code_blocks_run_in_order(relative: str) -> None: + namespace: dict[str, object] = {"__name__": "guide"} + for index, block in enumerate(_fences(_read(relative), "python")): + exec(compile(block, f"{relative}[{index}]", "exec"), namespace) + + +def test_dpp4fun_field_reference_matches_the_bundled_profile() -> None: + script = ROOT / "scripts" / "generate_dpp4fun_fields.py" + spec = importlib.util.spec_from_file_location("generate_dpp4fun_fields", script) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + assert _read("docs/dpp4fun-fields.md") == module.render(), ( + "docs/dpp4fun-fields.md is stale; run python scripts/generate_dpp4fun_fields.py" + ) diff --git a/tests/test_dpp4fun_final_parity.py b/tests/test_dpp4fun_final_parity.py new file mode 100644 index 0000000..e89356f --- /dev/null +++ b/tests/test_dpp4fun_final_parity.py @@ -0,0 +1,463 @@ +from __future__ import annotations + +import json +from datetime import UTC, datetime + +import pytest + +from dpp_sdk.core import ( + DataElementCollection, + DigitalProductPassport, + DppHeader, + Granularity, + MultiValuedDataElement, + SemanticPath, + SingleValuedDataElement, + ValidationSeverity, +) +from dpp_sdk.dpp4fun import ( + DPP4FUN_PROFILE, + BillOfMaterials, + CarbonFootprint, + ChairCharacteristics, + CribCharacteristics, + Dpp4Fun, + Dpp4FunJsonCodec, + Durability, + EnvironmentalImpact, + FabricCharacteristics, + HealthImpact, + LockerCharacteristics, + Nameplate, + ParticleBoardCharacteristics, + ProductRelationship, + Recyclability, + Refurbishment, + Reliability, + Remanufacturing, + Repairability, + Reusability, + RightOfEndUsers, + Safety, + SofaCharacteristics, + Traceability, + Upgradability, + Usage, + WasteManagement, +) +from dpp_sdk.dpp4fun.profile import Dpp4FunSemanticResolver +from dpp_sdk.dpp4fun.validation import validate_dpp4fun + + +def _single( + element_id: str, value: object, datatype: str = "xsd:string" +) -> SingleValuedDataElement: + return SingleValuedDataElement( + element_id=element_id, + object_type="SingleValuedDataElement", + value_data_type=datatype, + value=value, + ) + + +def _passport(*elements, specs: tuple[str, ...] = ("DPP4FUN_COMMON_V1",)) -> Dpp4Fun: + header = DppHeader( + digital_product_passport_id="urn:example:dpp:1", + unique_product_identifier="urn:example:product:1", + granularity=Granularity.MODEL, + dpp_schema_version="1.0", + dpp_status="active", + last_updated=datetime(2026, 1, 1, tzinfo=UTC), + economic_operator_id="urn:example:operator:1", + content_specification_ids=specs, + ) + return Dpp4Fun.from_passport( + DigitalProductPassport(dpp_header=header, data_elements=tuple(elements)) + ) + + +def _issues(value: Dpp4Fun) -> list[tuple[str, str, str]]: + return [(item.rule, item.path, item.severity.value) for item in validate_dpp4fun(value).issues] + + +def _profile_issues(value: Dpp4Fun) -> list[tuple[str, str, str]]: + from dpp_sdk.dpp4fun import Dpp4FunValidator + + return [ + (item.rule, item.path, item.severity.value) + for item in Dpp4FunValidator().validate(value).issues + ] + + +def _aspect(root_id: str, *children, specs=("DPP4FUN_COMMON_V1",)) -> Dpp4Fun: + return _passport( + DataElementCollection( + element_id=root_id, object_type="DataElementCollection", elements=children + ), + specs=specs, + ) + + +def test_dpp4fun_codec_full_compressed_full_uses_profile_resolver() -> None: + codec = Dpp4FunJsonCodec() + aspect = DPP4FUN_PROFILE + # Nameplate has source-backed multilingual fields and ordered children. + value = Dpp4Fun.create( + DppHeader( + digital_product_passport_id="urn:example:dpp:1", + unique_product_identifier="urn:example:product:1", + granularity=Granularity.MODEL, + dpp_schema_version="1.0", + dpp_status="active", + last_updated=datetime(2026, 1, 1, tzinfo=UTC), + economic_operator_id="urn:example:operator:1", + ), + Nameplate.create( + manufacturerProductDesignation={"en": "Chair"}, manufacturerName="Example" + ), + ) + full = codec.to_expanded_json(value) + compressed = codec.to_compressed_json(value) + restored = codec.from_compressed_json(compressed) + assert codec.from_expanded_json(full).passport == value.passport + assert restored.passport == value.passport + assert json.loads(codec.to_expanded_json(restored)) == json.loads(full) + assert aspect.metadata["profileVersion"] == "1.0.0" + + +@pytest.mark.parametrize( + "variant_type", + [ + ChairCharacteristics, + CribCharacteristics, + FabricCharacteristics, + LockerCharacteristics, + ParticleBoardCharacteristics, + SofaCharacteristics, + ], +) +def test_full_compressed_full_round_trip_for_each_characteristics_variant(variant_type) -> None: + codec = Dpp4FunJsonCodec() + applicability = f"characteristics-{variant_type.variant.value}" + definition = next( + item + for item in DPP4FUN_PROFILE.definitions + if item.path.startswith("characteristics.") + and item.path.count(".") == 1 + and applicability in item.applies_to + and item.object_type == "SingleValuedDataElement" + ) + scalar = { + "xsd:string": "sample", + "xsd:anyURI": "https://example.org/value", + "xsd:dateTime": "2026-01-01T00:00:00Z", + "xsd:boolean": False, + "xsd:integer": 1, + "xsd:double": 1.0, + "xsd:decimal": 1.0, + }[definition.value_data_type] + value = Dpp4Fun.create( + _passport().header, variant_type.create(**{definition.element_id: scalar}) + ) + restored = codec.from_compressed_json(codec.to_compressed_json(value)) + assert restored.passport == value.passport + + +def test_full_compressed_full_round_trip_preserves_decimal_json_semantics() -> None: + codec = Dpp4FunJsonCodec() + value = Dpp4Fun.create(_passport().header, ChairCharacteristics.create(height=0.1)) + restored = codec.from_compressed_json(codec.to_compressed_json(value)) + + # Core parsing retains decimal JSON numbers exactly; the DPP4Fun typed constructor uses float. + assert json.loads(codec.to_expanded_json(restored)) == json.loads(codec.to_expanded_json(value)) + assert restored.passport == value.passport + + +def test_compressed_unknown_extension_does_not_gain_fabricated_semantics() -> None: + extension = SingleValuedDataElement( + element_id="vendorField", + object_type="SingleValuedDataElement", + dictionary_reference="urn:vendor:term", + value_data_type="xsd:string", + value="present", + ) + value = _passport(extension) + codec = Dpp4FunJsonCodec() + expansion = codec.expand_compressed_json(codec.to_compressed_json(value)) + restored = expansion.passport.data_elements[0] + assert isinstance(restored, SingleValuedDataElement) + assert restored.dictionary_reference is None + assert restored.value_data_type is None + assert expansion.issues[0].rule == "EN18223_COMPRESSED_SEMANTICS_UNRESOLVED" + + +def test_profile_resolver_returns_only_selected_authoritative_definition() -> None: + resolver = Dpp4FunSemanticResolver() + selected = resolver.resolve( + ("DPP4FUN_COMMON_V1",), SemanticPath.of("nameplate", "manufacturerName") + ) + assert selected is not None and selected.element_id == "manufacturerName" + assert resolver.resolve(("UNKNOWN",), SemanticPath.of("nameplate", "manufacturerName")) is None + + +@pytest.mark.parametrize( + ("content_ids", "expected_rule"), + [ + (("DPP4FUN_COMMON_V1", "UNKNOWN"), "DPP4FUN_CONTENT_SPEC_SUPPORTED"), + ], +) +def test_content_profile_issues_are_ordered_and_stable( + content_ids: tuple[str, ...], expected_rule: str +) -> None: + result = validate_dpp4fun(_passport(specs=content_ids)) + assert any( + issue.rule == expected_rule and issue.severity is ValidationSeverity.ERROR + for issue in result.issues + ) + + +def test_aggregate_rejects_multiple_characteristics_content_profiles() -> None: + with pytest.raises(ValueError, match="only one Characteristics variant"): + _passport(specs=("DPP4FUN_COMMON_V1", "DPP4FUN_CHAIR_V1", "DPP4FUN_SOFA_V1")) + malformed = Dpp4Fun( + passport=DigitalProductPassport( + dpp_header=_passport().header.with_updates( + content_specification_ids=( + "DPP4FUN_COMMON_V1", + "DPP4FUN_CHAIR_V1", + "DPP4FUN_SOFA_V1", + ) + ) + ) + ) + assert any( + rule == "DPP4FUN_CHARACTERISTICS_VARIANT_SINGLE" + for rule, _, _ in _profile_issues(malformed) + ) + + +@pytest.mark.parametrize( + ("record_id", "children", "expected"), + [ + ( + "repairRecordSet", + ( + _single("name", "repair"), + _single("description", "fixed"), + _single("operator", "shop"), + _single("time", "2026-01-01T00:00:00Z"), + ), + [], + ), + ( + "designRecordSet", + ( + _single("name", "release"), + _single("description", "released"), + _single("operator", "team"), + _single("time", "2026-01-01T00:00:00Z"), + ), + [("DPP4FUN_RECORD_STATUS_REQUIRED", "traceability.designRecordSet[0].status", "ERROR")], + ), + ], +) +def test_record_minimums_preserve_repair_status_exception(record_id, children, expected) -> None: + root_id = "repairability" if record_id == "repairRecordSet" else "traceability" + records = MultiValuedDataElement( + element_id=record_id, + object_type="MultiValuedDataElement", + value=( + DataElementCollection( + element_id=f"{record_id[:-3]}_00", + object_type="DataElementCollection", + elements=children, + ), + ), + ) + value = _passport( + DataElementCollection( + element_id=root_id, object_type="DataElementCollection", elements=(records,) + ) + ) + assert [item for item in _issues(value) if item[0].startswith("DPP4FUN_RECORD_")] == expected + + +def test_unknown_extensions_are_retained_and_do_not_receive_guessed_profile_rules() -> None: + extension = _single("documentation", "custom") + value = _passport(extension) + result = validate_dpp4fun(value) + assert result.is_valid + assert value.passport.data_elements == (extension,) + + +@pytest.mark.parametrize( + "aspect_type", + [ + Nameplate, + HealthImpact, + EnvironmentalImpact, + Durability, + CarbonFootprint, + BillOfMaterials, + Refurbishment, + Recyclability, + ProductRelationship, + Remanufacturing, + Reliability, + Repairability, + Reusability, + WasteManagement, + Usage, + Upgradability, + Traceability, + Safety, + RightOfEndUsers, + ], +) +def test_each_general_aspect_uses_its_catalog_binding(aspect_type) -> None: + value = Dpp4Fun.create(_passport().header, aspect_type.create()) + assert any(rule == "DPP4FUN_EXPLICIT_CARDINALITY" for rule, _, _ in _profile_issues(value)) + + +@pytest.mark.parametrize( + "variant_type", + [ + ChairCharacteristics, + CribCharacteristics, + FabricCharacteristics, + LockerCharacteristics, + ParticleBoardCharacteristics, + SofaCharacteristics, + ], +) +def test_each_characteristics_variant_uses_its_selected_profile(variant_type) -> None: + value = Dpp4Fun.create(_passport().header, variant_type.create()) + assert any(rule == "DPP4FUN_EXPLICIT_CARDINALITY" for rule, _, _ in _profile_issues(value)) + + +@pytest.mark.parametrize( + ("present", "expected"), + [ + ({"description", "reference"}, "DPP4FUN_INSTRUCTION_NAME_REQUIRED"), + ({"name", "reference"}, "DPP4FUN_INSTRUCTION_DESCRIPTION_REQUIRED"), + ({"name", "description"}, "DPP4FUN_INSTRUCTION_REFERENCE_REQUIRED"), + ], +) +def test_instruction_minimum_fields(present, expected) -> None: + item = DataElementCollection( + element_id="repairInstruction_00", + object_type="DataElementCollection", + elements=tuple( + _single(key, "https://example.org/x" if key == "reference" else key) + for key in ("name", "description", "reference") + if key in present + ), + ) + value = _aspect( + "repairability", + MultiValuedDataElement( + element_id="repairInstructionSet", object_type="MultiValuedDataElement", value=(item,) + ), + ) + assert any(rule == expected for rule, _, _ in _profile_issues(value)) + + +@pytest.mark.parametrize( + ("present", "expected"), + [ + ({"description"}, "DPP4FUN_ASSESSMENT_NAME_REQUIRED"), + ({"name"}, "DPP4FUN_ASSESSMENT_DESCRIPTION_REQUIRED"), + ], +) +def test_assessment_minimum_fields(present, expected) -> None: + item = DataElementCollection( + element_id="scoring_00", + object_type="DataElementCollection", + elements=tuple(_single(key, key) for key in ("name", "description") if key in present), + ) + value = _aspect( + "durability", + MultiValuedDataElement( + element_id="scoringSet", object_type="MultiValuedDataElement", value=(item,) + ), + ) + assert any(rule == expected for rule, _, _ in _profile_issues(value)) + + +def test_semantic_binding_uri_unit_surface_and_professional_use_rules() -> None: + bad_gtin = SingleValuedDataElement( + element_id="gtinCode", + object_type="SingleValuedDataElement", + dictionary_reference="urn:wrong:dictionary", + value_data_type="xsd:integer", + value=42, + ) + issues = _profile_issues(_aspect("nameplate", bad_gtin)) + assert {rule for rule, _, _ in issues} >= { + "DPP4FUN_RESERVED_PATH_DICTIONARY_REFERENCE", + "DPP4FUN_RESERVED_PATH_VALUE_DATATYPE", + "DPP4FUN_BINDING_DATATYPE_MISMATCH", + } + + material = DataElementCollection( + element_id="material_wrong", + object_type="DataElementCollection", + elements=(_single("reference", "bad uri", "xsd:anyURI"),), + ) + bom = DataElementCollection( + element_id="billOfMaterials", + object_type="DataElementCollection", + elements=( + MultiValuedDataElement( + element_id="materialSet", + object_type="MultiValuedDataElement", + value_data_type="xsd:string", + value=(material,), + ), + ), + ) + assert bom.elements[0].element_id == "materialSet" + assert len(bom.elements[0].value) == 1 + issues = _profile_issues(_passport(bom)) + assert {rule for rule, _, _ in issues} >= { + "DPP4FUN_LIST_ITEM_ID_CANONICAL", + "DPP4FUN_URI_FIELD_RFC3986", + "DPP4FUN_RESERVED_PATH_VALUE_DATATYPE", + }, issues + + safety = _aspect("safety", _single("professionalUseOnly", "yes")) + assert any( + rule == "DPP4FUN_BINDING_DATATYPE_MISMATCH" for rule, _, _ in _profile_issues(safety) + ) + boolean_safety = Dpp4Fun.create(_passport().header, Safety.create(professionalUseOnly=True)) + assert not any( + rule == "DPP4FUN_BINDING_DATATYPE_MISMATCH" + for rule, _, _ in _profile_issues(boolean_safety) + ) + + specs = ("DPP4FUN_COMMON_V1", "DPP4FUN_CHAIR_V1") + chair = _aspect("characteristics", _single("lengthUnit", "kg"), specs=specs) + assert any(rule == "DPP4FUN_UNIT_DIMENSION_MISMATCH" for rule, _, _ in _profile_issues(chair)) + surface = _aspect("characteristics", _single("surface", 1.5, "xsd:double"), specs=specs) + assert surface.passport.data_elements[0].elements[0].value == 1.5 + assert {rule for rule, _, _ in _profile_issues(surface)} >= { + "DPP4FUN_RESERVED_PATH_VALUE_DATATYPE", + "DPP4FUN_BINDING_DATATYPE_MISMATCH", + } + + +def test_carbon_calculation_is_not_given_assessment_minima() -> None: + calculation = DataElementCollection( + element_id="carbonFootprintCalculation_00", + object_type="DataElementCollection", + elements=(_single("phase", "manufacturing"),), + ) + value = _aspect( + "carbonFootprint", + MultiValuedDataElement( + element_id="carbonFootprintCalculationSet", + object_type="MultiValuedDataElement", + value=(calculation,), + ), + ) + assert not any(rule.startswith("DPP4FUN_ASSESSMENT_") for rule, _, _ in _profile_issues(value)) diff --git a/tests/test_dpp4fun_model.py b/tests/test_dpp4fun_model.py deleted file mode 100644 index 3f90473..0000000 --- a/tests/test_dpp4fun_model.py +++ /dev/null @@ -1,301 +0,0 @@ -"""Dpp4Fun construction, immutable collections, and update contracts.""" - -from __future__ import annotations - -import pytest -from pydantic import ValidationError - -from dpp_sdk.dpp4fun.model import ( - BillOfMaterials, - Characteristics, - Component, - Dimensions, - Dpp4Fun, - Material, - Part, - ProductClassification, -) - - -@pytest.mark.parametrize( - "contract_id, factory", - [ - ("MODEL-DPP4FUN-BILL-OF-MATERIALS-CONSTRUCTION", BillOfMaterials), - ( - "MODEL-DPP4FUN-CHARACTERISTICS-CONSTRUCTION", - lambda: Characteristics(productName="Chair"), - ), - ( - "MODEL-DPP4FUN-CHARACTERISTICS-FIELD-COLOR", - lambda: Characteristics(productName="Chair", color="red"), - ), - ( - "MODEL-DPP4FUN-CHARACTERISTICS-FIELD-DESCRIPTION", - lambda: Characteristics(productName="Chair", description="oak"), - ), - ("MODEL-DPP4FUN-COMPONENT", lambda: Component(name="leg")), - ("MODEL-DPP4FUN-COMPONENT-CONSTRUCTION", lambda: Component(name="leg")), - ("MODEL-DPP4FUN-DIMENSIONS", lambda: Dimensions(width=1, height=2, depth=3)), - ("MODEL-DPP4FUN-DIMENSIONS-CONSTRUCTION", lambda: Dimensions(width=1, height=2, depth=3)), - ("MODEL-DPP4FUN-DPP4-FUN", lambda: None), - ("MODEL-DPP4FUN-DPP4-FUN-CONSTRUCTION", lambda: None), - ("MODEL-DPP4FUN-MATERIAL", lambda: Material(name="steel")), - ("MODEL-DPP4FUN-MATERIAL-CONSTRUCTION", lambda: Material(name="steel")), - ("MODEL-DPP4FUN-PART", lambda: Part(name="seat")), - ("MODEL-DPP4FUN-PART-CONSTRUCTION", lambda: Part(name="seat")), - ( - "MODEL-DPP4FUN-PRODUCT-CLASSIFICATION-CONSTRUCTION", - lambda: ProductClassification(sector="Furniture", category="Chair"), - ), - ( - "MODEL-DPP4FUN-PRODUCT-CLASSIFICATION-FIELD-SUB-CATEGORY", - lambda: ProductClassification( - sector="Furniture", category="Chair", subCategory="Office" - ), - ), - ], - ids=lambda row: row if isinstance(row, str) else None, -) -def test_remaining_dpp4fun_contract_evidence( - contract_id: str, factory: object, valid_dpp4fun: Dpp4Fun -) -> None: - if "DPP4-FUN" in contract_id: - model = valid_dpp4fun - assert type(model).model_validate(model.model_dump(mode="json")) == model - return - model = factory() # type: ignore[operator] - assert type(model).model_validate(model.model_dump(mode="json")) == model - - -@pytest.mark.parametrize( - ("contract_id", "model", "field"), - [ - ( - "MODEL-DPP4FUN-PRODUCT-CLASSIFICATION", - ProductClassification(sector="Furniture", category="Chair", tags=["a"]), - "tags", - ), - ( - "MODEL-DPP4FUN-CHARACTERISTICS", - Characteristics(productName="Chair", features=["a"]), - "features", - ), - ("MODEL-DPP4FUN-BILL-OF-MATERIALS", BillOfMaterials(), "materials"), - ], -) -def test_dpp4fun_collections_are_tuples_and_json_arrays( - contract_id: str, model: object, field: str -) -> None: - value = getattr(model, field) - assert isinstance(value, tuple) - assert model.model_dump(mode="json")[field] == list(value) # type: ignore[union-attr] - with pytest.raises(AttributeError): - value.append("x") - - -def test_dpp4fun_empty_collections_are_immutable_defaults() -> None: - assert BillOfMaterials().materials == () - - -@pytest.mark.parametrize( - "invalid", - [-1.0, float("nan"), float("inf"), float("-inf")], - ids=["negative", "nan", "positive-infinity", "negative-infinity"], -) -def test_dec_003_contracted_numbers_are_finite_and_non_negative(invalid: float) -> None: - with pytest.raises(ValidationError): - Characteristics(productName="Chair", weight=invalid) - with pytest.raises(ValidationError): - Dimensions(width=invalid, height=1.0, depth=1.0, unit="cm") - with pytest.raises(ValidationError): - Material(name="Steel", portion=invalid) - - -def test_dec_003_zero_positive_and_finite_exponent_values_are_accepted() -> None: - assert Characteristics(productName="Chair", weight=0.0).weight == 0.0 - assert Dimensions(width=1.25e2, height=1.0, depth=1.0, unit="cm").width == 125.0 - assert Material(name="Steel", portion=1.25e-2).portion == 0.0125 - - -@pytest.mark.parametrize( - ("contract_id", "attribute", "replacement"), - [ - ("MODEL-DPP4FUN-BILL-OF-MATERIALS-IMMUTABLE-UPDATE", "billOfMaterials", BillOfMaterials()), - ( - "MODEL-DPP4FUN-CHARACTERISTICS-IMMUTABLE-UPDATE", - "characteristics", - Characteristics(productName="Changed"), - ), - ( - "MODEL-DPP4FUN-DPP4-FUN-IMMUTABLE-UPDATE", - "classification", - ProductClassification(sector="Furniture", category="Changed"), - ), - ], -) -def test_dpp4fun_with_updates_preserves_subtype( - contract_id: str, attribute: str, replacement: object, valid_dpp4fun: object -) -> None: - updated = valid_dpp4fun.with_updates(**{attribute: replacement}) # type: ignore[union-attr] - assert type(updated) is type(valid_dpp4fun) - assert getattr(updated, attribute) == replacement - with pytest.raises(ValidationError): - valid_dpp4fun.with_updates(unknown=True) # type: ignore[union-attr] - - -@pytest.mark.parametrize( - ("contract_id", "model", "changes"), - [ - ("MODEL-DPP4FUN-COMPONENT-IMMUTABLE-UPDATE", Component(name="leg"), {"reference": "C-1"}), - ( - "MODEL-DPP4FUN-DIMENSIONS-IMMUTABLE-UPDATE", - Dimensions(width=1, height=1, depth=1), - {"unit": "cm"}, - ), - ("MODEL-DPP4FUN-MATERIAL-IMMUTABLE-UPDATE", Material(name="steel"), {"portion": 0.5}), - ("MODEL-DPP4FUN-PART-IMMUTABLE-UPDATE", Part(name="seat"), {"mandatory": True}), - ( - "MODEL-DPP4FUN-PRODUCT-CLASSIFICATION-IMMUTABLE-UPDATE", - ProductClassification(sector="Furniture", category="Chair"), - {"tags": ["office"]}, - ), - ], -) -def test_product_leaf_with_updates_revalidates( - contract_id: str, model: object, changes: dict[str, object] -) -> None: - updated = model.with_updates(**changes) # type: ignore[union-attr] - assert type(updated) is type(model) - with pytest.raises(ValidationError): - model.with_updates(unknown=True) # type: ignore[union-attr] - - -def test_dpp4fun_with_updates_rejects_invalid_known_field_values() -> None: - """The shared immutable update path revalidates known DPP4Fun fields.""" - classification = ProductClassification(sector="Furniture", category="Chair") - - with pytest.raises(ValidationError, match="must not be blank if provided"): - classification.with_updates(group=" ") - - -@pytest.mark.parametrize( - ( - "contract_id", - "model", - "valid_changes", - "changed_field", - "expected_value", - "invalid_changes", - "invalid_match", - ), - [ - pytest.param( - "MODEL-DPP4FUN-BILL-OF-MATERIALS-IMMUTABLE-UPDATE", - BillOfMaterials(), - {"materials": (Material(name="Steel", portion=1.0),)}, - "materials", - (Material(name="Steel", portion=1.0),), - {"materials": (None,)}, - "materials.0", - id="MODEL-DPP4FUN-BILL-OF-MATERIALS-IMMUTABLE-UPDATE", - ), - pytest.param( - "MODEL-DPP4FUN-CHARACTERISTICS-IMMUTABLE-UPDATE", - Characteristics(productName="Chair"), - {"productName": "Desk"}, - "productName", - "Desk", - {"productName": " "}, - "must not be blank", - id="MODEL-DPP4FUN-CHARACTERISTICS-IMMUTABLE-UPDATE", - ), - pytest.param( - "MODEL-DPP4FUN-COMPONENT-IMMUTABLE-UPDATE", - Component(name="Leg"), - {"reference": "C-1"}, - "reference", - "C-1", - {"name": " "}, - "must not be blank", - id="MODEL-DPP4FUN-COMPONENT-IMMUTABLE-UPDATE", - ), - pytest.param( - "MODEL-DPP4FUN-DIMENSIONS-IMMUTABLE-UPDATE", - Dimensions(width=1.0, height=2.0, depth=3.0), - {"unit": "cm"}, - "unit", - "cm", - {"width": -1.0}, - "greater than or equal to 0", - id="MODEL-DPP4FUN-DIMENSIONS-IMMUTABLE-UPDATE", - ), - pytest.param( - "MODEL-DPP4FUN-DPP4-FUN-IMMUTABLE-UPDATE", - None, - { - "classification": ProductClassification( - sector="Furniture", - category="Desk", - ) - }, - "classification", - ProductClassification(sector="Furniture", category="Desk"), - {"coreDpp": None}, - "coreDpp", - id="MODEL-DPP4FUN-DPP4-FUN-IMMUTABLE-UPDATE", - ), - pytest.param( - "MODEL-DPP4FUN-MATERIAL-IMMUTABLE-UPDATE", - Material(name="Steel"), - {"portion": 0.5}, - "portion", - 0.5, - {"portion": -1.0}, - "greater than or equal to 0", - id="MODEL-DPP4FUN-MATERIAL-IMMUTABLE-UPDATE", - ), - pytest.param( - "MODEL-DPP4FUN-PART-IMMUTABLE-UPDATE", - Part(name="Seat"), - {"mandatory": True}, - "mandatory", - True, - {"name": " "}, - "must not be blank", - id="MODEL-DPP4FUN-PART-IMMUTABLE-UPDATE", - ), - pytest.param( - "MODEL-DPP4FUN-PRODUCT-CLASSIFICATION-IMMUTABLE-UPDATE", - ProductClassification(sector="Furniture", category="Chair"), - {"tags": ("office",)}, - "tags", - ("office",), - {"group": " "}, - "must not be blank if provided", - id="MODEL-DPP4FUN-PRODUCT-CLASSIFICATION-IMMUTABLE-UPDATE", - ), - ], -) -def test_dpp4fun_contract_with_updates_revalidates_known_fields_and_preserves_original( - contract_id: str, - model: object, - valid_changes: dict[str, object], - changed_field: str, - expected_value: object, - invalid_changes: dict[str, object], - invalid_match: str, - valid_dpp4fun: Dpp4Fun, -) -> None: - value = valid_dpp4fun if model is None else model - before = value.model_dump(mode="python") # type: ignore[union-attr] - original_field = getattr(value, changed_field) - - updated = value.with_updates(**valid_changes) # type: ignore[union-attr] - - assert type(updated) is type(value) - assert getattr(updated, changed_field) == expected_value - assert getattr(value, changed_field) == original_field - assert value.model_dump(mode="python") == before # type: ignore[union-attr] - with pytest.raises(ValidationError, match=invalid_match): - value.with_updates(**invalid_changes) # type: ignore[union-attr] - assert value.model_dump(mode="python") == before # type: ignore[union-attr] diff --git a/tests/test_dpp4fun_typed_profile.py b/tests/test_dpp4fun_typed_profile.py new file mode 100644 index 0000000..d9c12d5 --- /dev/null +++ b/tests/test_dpp4fun_typed_profile.py @@ -0,0 +1,418 @@ +from __future__ import annotations + +import pytest + +from dpp_sdk.core.model import ( + DataElementCollection, + DigitalProductPassport, + DppHeader, + Granularity, + SingleValuedDataElement, +) +from dpp_sdk.dpp4fun import composites +from dpp_sdk.dpp4fun.characteristics import ( + ChairCharacteristics, + CribCharacteristics, + FabricCharacteristics, + LockerCharacteristics, + ParticleBoardCharacteristics, + SofaCharacteristics, +) +from dpp_sdk.dpp4fun.profile import DPP4FUN_PROFILE, profile_definition +from dpp_sdk.dpp4fun.typed_model import BillOfMaterials, Dpp4Fun, Nameplate +from dpp_sdk.dpp4fun.units import Dpp4FunQuantity, LengthUnit, MassUnit + + +def test_packaged_profile_retains_its_complete_definition_set() -> None: + assert len(DPP4FUN_PROFILE.definitions) == 488 + assert ( + profile_definition("billOfMaterials.materialSet[*]").repeated_item_policy[ + "elementIdPattern" + ] + == "material_%02d" + ) + + +@pytest.mark.parametrize( + ("aspect_type", "element_id"), + [ + (cls, aspect_id) + for cls, aspect_id in [ + # All general aspects in the Java profile root order. + ("Nameplate", "nameplate"), + ("HealthImpact", "healthImpact"), + ("EnvironmentalImpact", "environmentalImpact"), + ("Durability", "durability"), + ("CarbonFootprint", "carbonFootprint"), + ("BillOfMaterials", "billOfMaterials"), + ("Refurbishment", "refurbishment"), + ("Recyclability", "recyclability"), + ("ProductRelationship", "productRelationship"), + ("Remanufacturing", "remanufacturing"), + ("Reliability", "reliability"), + ("Repairability", "repairability"), + ("Reusability", "reusability"), + ("WasteManagement", "wasteManagement"), + ("Usage", "usage"), + ("Upgradability", "upgradability"), + ("Traceability", "traceability"), + ("Safety", "safety"), + ("RightOfEndUsers", "rightOfEndUsers"), + ] + ], +) +def test_all_general_aspect_types_construct_profiled_empty_collections( + aspect_type: str, element_id: str +) -> None: + from dpp_sdk.dpp4fun import typed_model as model + + aspect_class = getattr(model, aspect_type) + aspect = aspect_class.create() + assert aspect.element.element_id == element_id + assert aspect.element.object_type == "DataElementCollection" + assert ( + aspect.element.dictionary_reference == profile_definition(element_id).dictionary_reference + ) + + +@pytest.mark.parametrize( + ("variant_type", "variant"), + [ + (ChairCharacteristics, "chair"), + (CribCharacteristics, "crib"), + (FabricCharacteristics, "fabric"), + (LockerCharacteristics, "locker"), + (ParticleBoardCharacteristics, "particleBoard"), + (SofaCharacteristics, "sofa"), + ], +) +def test_characteristics_variants_build_typed_semantic_values(variant_type, variant) -> None: + aspect = variant_type.create() + assert aspect.variant.value == variant + with_value = aspect.with_field("color", "oak") + color = next(e for e in with_value.element.elements if e.element_id == "color") + assert isinstance(color, SingleValuedDataElement) + assert color.value_data_type == "xsd:string" + assert ( + color.dictionary_reference + == profile_definition("characteristics.color", variant=variant).dictionary_reference + ) + with pytest.raises(TypeError): + aspect.with_field("color", 12) + + +@pytest.mark.parametrize( + ("variant_type", "field", "value", "datatype"), + [ + (ChairCharacteristics, "seatingCapacity", 2, "xsd:integer"), + (CribCharacteristics, "mattressIncluded", True, "xsd:boolean"), + (FabricCharacteristics, "flameRetardant", True, "xsd:boolean"), + (LockerCharacteristics, "lockIncluded", True, "xsd:boolean"), + (ParticleBoardCharacteristics, "carb2Epa", True, "xsd:boolean"), + (SofaCharacteristics, "headrestIncluded", True, "xsd:boolean"), + ], +) +def test_all_characteristics_variants_keep_java_public_field_types( + variant_type, field: str, value: object, datatype: str +) -> None: + aspect = variant_type.create(**{field: value}) + child = aspect.element.elements[0] + expected_name = "matelasIncluded" if field == "mattressIncluded" else field + assert child.element_id == expected_name + assert child.value_data_type == datatype + assert child.value is value + + +@pytest.mark.parametrize( + "variant_type", + [ + ChairCharacteristics, + CribCharacteristics, + FabricCharacteristics, + LockerCharacteristics, + ParticleBoardCharacteristics, + SofaCharacteristics, + ], +) +def test_surface_preserves_published_descriptive_string(variant_type) -> None: + aspect = variant_type.create(surface="Small-structured chenille fabric") + surface = aspect.field("surface") + assert isinstance(surface, SingleValuedDataElement) + assert surface.value == "Small-structured chenille fabric" + assert isinstance(surface.value, str) + assert surface.value_data_type == "xsd:string" + + +@pytest.mark.parametrize( + "value", + [ + 1, + 1.25, + True, + ], +) +@pytest.mark.parametrize("variant_type", [ChairCharacteristics, SofaCharacteristics]) +def test_surface_requires_descriptive_string_input(variant_type, value: object) -> None: + with pytest.raises(TypeError, match="characteristics.surface requires a string"): + variant_type.create(surface=value) + + +def test_surface_numeric_looking_string_is_preserved_without_conversion() -> None: + surface = ChairCharacteristics.create(surface="1.25").field("surface") + assert isinstance(surface, SingleValuedDataElement) + assert surface.value == "1.25" + assert surface.value_data_type == "xsd:string" + + +def test_surface_data_element_input_is_not_coerced() -> None: + source = SingleValuedDataElement( + element_id="surface", + object_type="SingleValuedDataElement", + value="Woven matte", + value_data_type="xsd:string", + ) + aspect = ChairCharacteristics.create(surface=source) + surface = aspect.field("surface") + assert isinstance(surface, SingleValuedDataElement) + assert surface.value == "Woven matte" + assert surface.value_data_type == "xsd:string" + assert source.value == "Woven matte" + assert source.value_data_type == "xsd:string" + + +def test_dpp4fun_has_one_canonical_passport_and_preserves_unknowns_on_edit() -> None: + header = DppHeader( + digital_product_passport_id="urn:example:passport:1", + unique_product_identifier="urn:example:product:1", + granularity=Granularity.MODEL, + ) + unknown = DataElementCollection(element_id="extension", object_type="DataElementCollection") + original = Dpp4Fun.create( + header, Nameplate.create(gtinCode="00012345678905"), extensions=(unknown,) + ) + updated = original.with_updates(header=header.with_updates(dpp_status="active")) + assert original.as_digital_product_passport().data_elements[-1] == unknown + assert updated.as_digital_product_passport().data_elements[-1] == unknown + assert updated.as_digital_product_passport().dpp_header.dpp_status == "active" + assert updated.header.dpp_schema_version == "1.0" + assert updated.header.last_updated is not None + assert updated.as_digital_product_passport() is not original.as_digital_product_passport() + assert set(Dpp4Fun.model_fields) == {"passport"} + + +def test_dpp4fun_edit_rejects_duplicate_aspect_ids_without_dropping_an_update() -> None: + header = DppHeader( + digital_product_passport_id="urn:example:passport:duplicate", + unique_product_identifier="urn:example:product:duplicate", + granularity=Granularity.MODEL, + ) + original = Dpp4Fun.create(header, Nameplate.create(brand="Cir4Fun")) + with pytest.raises(ValueError, match="unique element IDs"): + original.with_updates( + aspects=(Nameplate.create(brand="First"), Nameplate.create(brand="Second")) + ) + + +def test_profile_field_order_and_variant_access_follow_the_catalog() -> None: + nameplate = Nameplate.create(brand="Cir4Fun", gtinCode="04012345678901") + expected = [ + definition.element_id + for definition in DPP4FUN_PROFILE.definitions + if definition.path.rpartition(".")[0] == "nameplate" + and definition.element_id in {"brand", "gtinCode"} + ] + assert [field.element_id for field in nameplate.element.elements] == expected + header = DppHeader( + digital_product_passport_id="urn:example:passport:3", + unique_product_identifier="urn:example:product:3", + granularity=Granularity.MODEL, + ) + dpp = Dpp4Fun.create(header, ChairCharacteristics.create(color="blue")) + assert isinstance(dpp.selected_characteristics(), ChairCharacteristics) + + +def test_aspect_edits_preserve_unknown_children_without_adding_metadata() -> None: + original = Nameplate.create(brand="Cir4Fun") + extension = SingleValuedDataElement( + element_id="vendorCode", object_type="SingleValuedDataElement", value="X-1" + ) + source = original.element.with_updates(elements=(*original.element.elements, extension)) + edited = Nameplate.from_element(source).with_field("brand", "Example") + assert edited.field("brand").value == "Example" + assert ( + next(child for child in edited.element.elements if child.element_id == "vendorCode") + == extension + ) + assert extension.dictionary_reference is None + + +def test_quantity_pairs_numeric_value_with_dimension_checked_unit() -> None: + length = Dpp4FunQuantity(value=1.25, unit=LengthUnit.METER) + assert length.value == 1.25 + assert length.unit.symbol == "m" + with pytest.raises(ValueError): + Dpp4FunQuantity(value=1, unit=MassUnit.KILOGRAM, dimension="length") + + +def test_characteristic_quantities_use_profile_unit_slots_and_dimensions() -> None: + chair = ChairCharacteristics.create().with_quantity( + "height", Dpp4FunQuantity(value=80.0, unit=LengthUnit.CENTIMETER) + ) + assert chair.quantity("height") == Dpp4FunQuantity(value=80.0, unit=LengthUnit.CENTIMETER) + with pytest.raises(ValueError, match="DPP4FUN_UNIT_DIMENSION_MISMATCH"): + chair.with_quantity("weight", Dpp4FunQuantity(value=5.0, unit=LengthUnit.METER)) + with pytest.raises(ValueError, match="DPP4FUN_UNIT_SLOT_CONFLICT"): + chair.with_quantity("seatHeight", Dpp4FunQuantity(value=45.0, unit=LengthUnit.METER)) + + +def test_aggregate_rejects_multiple_selected_characteristics_variants() -> None: + header = DppHeader( + digital_product_passport_id="urn:example:passport:2", + unique_product_identifier="urn:example:product:2", + granularity=Granularity.MODEL, + content_specification_ids=("DPP4FUN_CHAIR_V1", "DPP4FUN_SOFA_V1"), + ) + with pytest.raises(ValueError, match="only one Characteristics variant"): + Dpp4Fun.from_passport(DigitalProductPassport(dpp_header=header)) + + +@pytest.mark.parametrize( + "composite_name", + [ + "Accessory", + "Address", + "CollectionMember", + "Certificate", + "CarbonFootprintCalculation", + "DesignRecord", + "Contact", + "Email", + "HealthIndicator", + "Component", + "ManufacturingRecord", + "Instruction", + "Indicator", + "Part", + "Material", + "Policies", + "PostSaleService", + "ProductFunction", + "TrackingRecord", + "Telephone", + "StandardAssessment", + "RepairRecord", + "Scoring", + ], +) +def test_all_java_reusable_composite_views_preserve_core_element(composite_name: str) -> None: + composite_type = getattr(composites, composite_name) + created = composite_type.create() + assert created.element.object_type == "DataElementCollection" + element = DataElementCollection( + element_id=created.element.element_id, object_type="DataElementCollection" + ) + composite = composite_type.from_element(element) + assert composite.as_data_element() is element + + +def test_material_composite_uses_profile_datatypes_and_dictionary_metadata() -> None: + material = composites.Material.create(name="Oak", portion=0.75, mandatory=True) + assert material.element.element_id == "material" + values = {item.element_id: item for item in material.element.elements} + assert values["name"].value_data_type == "xsd:string" + assert values["portion"].value_data_type == "xsd:double" + assert values["name"].dictionary_reference == "https://schema.org/name" + + +@pytest.mark.parametrize( + ("composite_name", "fields"), + [ + ("Address", {"country": "DE", "town": "Berlin"}), + ( + "Contact", + { + "organization": "Support", + "address": composites.Address.create(country="DE", town="Berlin").element, + }, + ), + ("Email", {"emailAddress": "help@example.test"}), + ("Telephone", {"telephoneNumber": "+4930123"}), + ("Component", {"name": "Frame", "mandatory": False}), + ("Part", {"name": "Leg", "mandatory": True}), + ("Certificate", {"name": "EN test", "issueDate": "2026-01-01T00:00:00Z"}), + ("HealthIndicator", {"name": "Formaldehyde", "value": "low"}), + ("Instruction", {"name": "Assembly", "targetAudience": "professional"}), + ("ProductFunction", {"name": "seating", "status": "primary"}), + ("StandardAssessment", {"name": "EN test", "complianceStatus": "compliant"}), + ("Policies", {"permissions": ["repair", "reuse"]}), + ], +) +def test_reusable_composites_construct_profile_typed_fields( + composite_name: str, fields: dict[str, object] +) -> None: + composite = getattr(composites, composite_name).create(**fields) + expected = [ + profile_definition( + f"{composite.profile_path}.{composite.field_aliases.get(name, name)}" + ).element_id + for name in fields + ] + assert [element.element_id for element in composite.element.elements] == expected + + +def test_composite_field_edits_are_immutable_and_keep_sibling_values() -> None: + original = composites.Material.create(name="Oak", portion=0.75) + updated = original.with_field("name", "Beech") + assert original.field("name").value == "Oak" + assert updated.field("name").value == "Beech" + assert updated.field("portion").value == 0.75 + + +def test_repeated_composite_item_ids_and_input_order_follow_profile() -> None: + first = composites.Material.create(name="Oak") + second = composites.Material.create(name="Beech") + aspect = BillOfMaterials.create(materialSet=(first.element, second.element)) + repeated = next(item for item in aspect.element.elements if item.element_id == "materialSet") + assert [item.element_id for item in repeated.value] == ["material_00", "material_01"] + assert [item.elements[0].value for item in repeated.value] == ["Oak", "Beech"] + + +def test_surface_profile_matches_corrected_java_binding_metadata() -> None: + assert DPP4FUN_PROFILE.metadata["sources"]["d4Version"] == "1.0" + conflicts = {item["id"]: item for item in DPP4FUN_PROFILE.conflicts} + assert { + "CHARACTERISTICS_SURFACE", + "CHARACTERISTICS_SURFACE_DICTIONARY", + } <= conflicts.keys() + assert conflicts["CHARACTERISTICS_SURFACE"]["status"] == "RESOLVED_WITH_SOURCE_CONFLICT" + assert "published AAS String binding" in conflicts["CHARACTERISTICS_SURFACE"]["note"] + assert ( + "Do not add a SurfaceArea field or semantic IRI" + in conflicts["CHARACTERISTICS_SURFACE_DICTIONARY"]["note"] + ) + aas_templates = { + "chair": "CIR4FUN.ChairCharacteristics.json", + "crib": "CIR4FUN.CribCharacteristics.json", + "fabric": "CIR4FUN.FabricCharacteristics.json", + "locker": "CIR4FUN.LockerCharacteristics.json", + "particleBoard": "CIR4FUN.ParticleBoardCharacteristics.json", + "sofa": "CIR4FUN.SofaCharacteristics.json", + } + for variant, template in aas_templates.items(): + surface = profile_definition("characteristics.surface", variant=variant) + assert surface.value_data_type == "xsd:string" + assert surface.dictionary_reference is None + assert surface.source["aasValueType"] == "xs:string" + assert surface.source["dpp4funCommit"] == "2007a574b0e35eae5997c9460bf4cf531588405b" + assert surface.source_references[0]["aasTemplate"] == template + assert surface.resolution_status == "RESOLVED_WITHOUT_DICTIONARY_REFERENCE" + assert surface.conflict_resolution is not None + assert "published AAS" in surface.conflict_resolution["note"] + assert profile_definition("safety.professionalUseOnly").value_data_type == "xsd:boolean" + for path in ( + "durability.standardSet[*].complianceStatus", + "recyclability.standardSet[*].complianceStatus", + "reliability.standardSet[*].complianceStatus", + "safety.standardSet[*].complianceStatus", + ): + assert profile_definition(path).value_data_type == "xsd:string" diff --git a/tests/test_dpp4fun_validation.py b/tests/test_dpp4fun_validation.py index d0ee52a..aa04c53 100644 --- a/tests/test_dpp4fun_validation.py +++ b/tests/test_dpp4fun_validation.py @@ -1,501 +1,39 @@ -"""Semantic validation of the furniture model, including cross-object rules.""" +"""Ordered source-backed DPP4Fun validation composition.""" from __future__ import annotations -from collections.abc import Callable -from datetime import date, timedelta -from typing import Any +from dpp_sdk.core import DigitalProductPassport, DppHeader, Granularity, ValidationSeverity +from dpp_sdk.dpp4fun import Dpp4Fun, Dpp4FunValidationService, Dpp4FunValidator -import pytest -from dpp_sdk.core.errors import DppValidationError -from dpp_sdk.core.model import PassportMetadata -from dpp_sdk.dpp4fun.model import ( - BillOfMaterials, - Characteristics, - Component, - Dimensions, - Dpp4Fun, - Material, - Part, - ProductClassification, -) -from dpp_sdk.dpp4fun.validation import ( - validate_bill_of_materials, - validate_characteristics, - validate_component, - validate_dimensions, - validate_dpp4fun, - validate_material, - validate_part, - validate_product_classification, -) - - -def test_valid_dpp_passes(valid_dpp4fun: Dpp4Fun) -> None: - validate_dpp4fun(valid_dpp4fun) # no raise - - -def test_dimensions_require_unit_when_values_present() -> None: - with pytest.raises(DppValidationError, match="Dimensions.unit"): - validate_dimensions(Dimensions(width=1.0, height=1.0, depth=1.0)) - - -def test_material_mandatory_requires_positive_portion() -> None: - with pytest.raises(DppValidationError, match="mandatory"): - validate_material(Material(name="glue", mandatory=True, portion=0.0)) - - -def test_bom_duplicate_by_name_and_reference_rejected() -> None: - bom = BillOfMaterials( - materials=( - Material(name="steel", portion=0.5, reference="R1"), - Material(name="Steel", portion=0.5, reference="r1"), # case-insensitive dup - ) - ) - with pytest.raises(DppValidationError, match="duplicate"): - validate_bill_of_materials(bom) - - -def test_bom_distinct_references_ok() -> None: - bom = BillOfMaterials( - components=(Component(name="leg", reference="L1"), Component(name="leg", reference="L2")) - ) - validate_bill_of_materials(bom) # no raise - - -@pytest.mark.parametrize( - ("bom", "indexed_path"), - [ - pytest.param( - BillOfMaterials.model_construct(materials=(None,), components=(), parts=()), - r"BillOfMaterials\.materials\[0\] must not be null", - id="materials-first", - ), - pytest.param( - BillOfMaterials.model_construct( - materials=(Material(name="Steel", portion=1.0), None), - components=(), - parts=(), - ), - r"BillOfMaterials\.materials\[1\] must not be null", - id="materials-later", - ), - pytest.param( - BillOfMaterials.model_construct(materials=(), components=(None,), parts=()), - r"BillOfMaterials\.components\[0\] must not be null", - id="components-first", - ), - pytest.param( - BillOfMaterials.model_construct( - materials=(), - components=(Component(name="Leg"), None), - parts=(), - ), - r"BillOfMaterials\.components\[1\] must not be null", - id="components-later", - ), - pytest.param( - BillOfMaterials.model_construct(materials=(), components=(), parts=(None,)), - r"BillOfMaterials\.parts\[0\] must not be null", - id="parts-first", - ), - pytest.param( - BillOfMaterials.model_construct( - materials=(), - components=(), - parts=(Part(name="Seat"), None), - ), - r"BillOfMaterials\.parts\[1\] must not be null", - id="parts-later", - ), - ], -) -def test_bom_null_member_raises_indexed_sdk_validation_error( - bom: BillOfMaterials, - indexed_path: str, -) -> None: - with pytest.raises(DppValidationError, match=indexed_path): - validate_bill_of_materials(bom) - - -@pytest.mark.parametrize( - ("bom", "first_error"), - [ - pytest.param( - BillOfMaterials.model_construct( - materials=(None,), - components=(None,), - parts=(None,), - ), - r"BillOfMaterials\.materials\[0\] must not be null", - id="materials-before-components-and-parts", - ), - pytest.param( - BillOfMaterials.model_construct( - materials=(Material(name="Steel", portion=1.0),), - components=(None,), - parts=(None,), - ), - r"BillOfMaterials\.components\[0\] must not be null", - id="components-before-parts", - ), - pytest.param( - BillOfMaterials.model_construct( - materials=(Material(name="Steel", portion=1.0),), - components=(Component(name="Leg"),), - parts=(None,), - ), - r"BillOfMaterials\.parts\[0\] must not be null", - id="parts-after-valid-earlier-collections", - ), - ], -) -def test_bom_null_member_preserves_collection_fail_fast_precedence( - bom: BillOfMaterials, - first_error: str, -) -> None: - with pytest.raises(DppValidationError, match=first_error): - validate_bill_of_materials(bom) - - -def test_tags_reject_duplicates() -> None: - classification = ProductClassification(sector="F", category="Chair", tags=("a", "A")) - with pytest.raises(DppValidationError, match="duplicate"): - validate_product_classification(classification) - - -def test_dec_002_duplicate_normalization_uses_only_frozen_whitespace_table() -> None: - with pytest.raises(DppValidationError, match="duplicate"): - validate_product_classification( - ProductClassification( - sector="F", - category="Chair", - tags=("Chair", "\u00a0chair\u2003"), - ) - ) - - validate_product_classification( - ProductClassification( - sector="F", - category="Chair", - tags=("Chair", "\u001cchair"), - ) - ) - - -@pytest.mark.parametrize( - "invalid", - [float("nan"), float("inf"), float("-inf")], - ids=["nan", "positive-infinity", "negative-infinity"], -) -def test_dec_003_semantic_numeric_checks_reject_non_finite_bypass(invalid: float) -> None: - with pytest.raises(DppValidationError, match="Characteristics.weight"): - validate_characteristics( - Characteristics.model_construct(productName="Chair", weight=invalid) - ) - with pytest.raises(DppValidationError, match="Dimensions.width"): - validate_dimensions( - Dimensions.model_construct(width=invalid, height=1.0, depth=1.0, unit="cm") - ) - with pytest.raises(DppValidationError, match="Material.portion"): - validate_material(Material.model_construct(name="Steel", portion=invalid)) - - -def test_features_reject_blank() -> None: - characteristics = Characteristics(productName="X", features=("ok", " ")) - with pytest.raises(DppValidationError, match="features"): - validate_characteristics(characteristics) - - -def test_cross_rule_category_producttype_inconsistent(valid_dpp4fun: Dpp4Fun) -> None: - bad = valid_dpp4fun.model_copy( - update={ - "characteristics": valid_dpp4fun.characteristics.model_copy( - update={"productType": "Table"} - ) - } - ) - with pytest.raises(DppValidationError, match="inconsistent"): - validate_dpp4fun(bad) - - -def test_cross_rule_external_link_requires_documentation(valid_dpp4fun: Dpp4Fun) -> None: - core = valid_dpp4fun.coreDpp - core_no_doc = core.model_copy( - update={ - "documentation": None, - "passportMetadata": core.passportMetadata.model_copy( - update={"externalDocumentationLink": "https://x.example"} - ), - } - ) - bad = valid_dpp4fun.model_copy(update={"coreDpp": core_no_doc}) - with pytest.raises(DppValidationError, match="externalDocumentationLink"): - validate_dpp4fun(bad) - - -def test_dec_005_supported_aggregate_validation_order(valid_dpp4fun: Dpp4Fun) -> None: - invalid_metadata = valid_dpp4fun.passportMetadata.model_copy( - update={"passportUpdateDates": (date.today() + timedelta(days=1),)} +def test_validation_service_reports_core_then_profile_findings() -> None: + passport = Dpp4Fun.from_passport( + DigitalProductPassport(dpp_header=DppHeader(content_specification_ids=("unknown-profile",))) ) - invalid_core = valid_dpp4fun.coreDpp.model_copy(update={"passportMetadata": invalid_metadata}) - invalid_classification = valid_dpp4fun.classification.model_copy( - update={"tags": ("Chair", "chair")} - ) - invalid_characteristics = valid_dpp4fun.characteristics.model_copy(update={"weight": -1.0}) - - core_first = valid_dpp4fun.model_copy( - update={ - "coreDpp": invalid_core, - "classification": invalid_classification, - "characteristics": invalid_characteristics, - } - ) - with pytest.raises(DppValidationError, match=r"passportUpdateDates\[0\]"): - validate_dpp4fun(core_first) - - classification_first = valid_dpp4fun.model_copy( - update={ - "classification": invalid_classification, - "characteristics": invalid_characteristics, - } - ) - with pytest.raises(DppValidationError, match="duplicate entry"): - validate_dpp4fun(classification_first) - - -@pytest.mark.parametrize( - ("contract_id", "validator", "value", "context"), - [ - pytest.param( - "VALIDATION-CORE-VALIDATION-UTILS--A517C1", - validate_product_classification, - ProductClassification.model_construct(sector="F", category="Chair", tags=(None,)), - r"tags\[0\] must not be null", - id="VALIDATION-CORE-VALIDATION-UTILS--A517C1", - ), - pytest.param( - "VALIDATION-CORE-VALIDATION-UTILS--E376BD", - validate_characteristics, - Characteristics.model_construct(productName="Chair", features=(" ",)), - r"features\[0\] must not be blank", - id="VALIDATION-CORE-VALIDATION-UTILS--E376BD", - ), - pytest.param( - "VALIDATION-CORE-VALIDATION-UTILS-CONTAINS-DUPLICATE-ENTRY-2E8F79", - validate_product_classification, - ProductClassification(sector="F", category="Chair", tags=("a", " A ")), - "duplicate", - id="VALIDATION-CORE-VALIDATION-UTILS-CONTAINS-DUPLICATE-ENTRY-2E8F79", - ), - pytest.param( - "VALIDATION-DPP4FUN-COMPONENT-COMPONENT-VALIDATOR-CBB620", - validate_bill_of_materials, - BillOfMaterials(components=(Component.model_construct(name=""),)), - "Component.name", - id="VALIDATION-DPP4FUN-COMPONENT-COMPONENT-VALIDATOR-CBB620", - ), - pytest.param( - "VALIDATION-DPP4FUN-PART-PART-VALIDATOR-247ACF", - validate_part, - Part.model_construct(name=""), - "Part.name", - id="VALIDATION-DPP4FUN-PART-PART-VALIDATOR-247ACF", - ), - pytest.param( - "VALIDATION-CORE-VALIDATION-UTILS-IS-REQUIRED-868AB4", - validate_product_classification, - None, - "ProductClassification is required", - id="VALIDATION-CORE-VALIDATION-UTILS-IS-REQUIRED-868AB4", - ), - pytest.param( - "VALIDATION-CORE-VALIDATION-UTILS-MUST-BE-NON-NEGATIVE-BUT-GOT-4EE214", - validate_characteristics, - Characteristics.model_construct(productName="Chair", weight=-1.0), - "Characteristics.weight", - id="VALIDATION-CORE-VALIDATION-UTILS-MUST-BE-NON-NEGATIVE-BUT-GOT-4EE214", - ), - pytest.param( - "VALIDATION-CORE-VALIDATION-UTILS-MUST-BE-NON-NEGATIVE-BUT-GOT-C12841", - validate_dimensions, - Dimensions.model_construct(width=-1.0, height=1.0, depth=1.0, unit="cm"), - "Dimensions.width", - id="VALIDATION-CORE-VALIDATION-UTILS-MUST-BE-NON-NEGATIVE-BUT-GOT-C12841", - ), - pytest.param( - "VALIDATION-CORE-VALIDATION-UTILS-MUST-BE-NON-NEGATIVE-BUT-GOT-C93495", - validate_material, - Material.model_construct(name="Steel", portion=-1.0), - "Material.portion", - id="VALIDATION-CORE-VALIDATION-UTILS-MUST-BE-NON-NEGATIVE-BUT-GOT-C93495", - ), - pytest.param( - "VALIDATION-CORE-VALIDATION-UTILS-MUST-NOT-BE-BLANK-281609", - validate_component, - Component.model_construct(name=None), - "Component.name", - id="VALIDATION-CORE-VALIDATION-UTILS-MUST-NOT-BE-BLANK-281609", - ), - pytest.param( - "VALIDATION-CORE-VALIDATION-UTILS-VALIDATION-RULE-DPP-DATAMODEL-DPPSDK-CORE-VALIDATION-VA-C54B7E", - validate_material, - Material(name="Glue", mandatory=True, portion=0.0), - "mandatory", - id="VALIDATION-CORE-VALIDATION-UTILS-VALIDATION-RULE-DPP-DATAMODEL-DPPSDK-CORE-VALIDATION-VA-C54B7E", - ), - pytest.param( - "VALIDATION-DPP4FUN-COMPONENT-COMPONENT-NAME-FDA338", - validate_component, - Component.model_construct(name=""), - "Component.name", - id="VALIDATION-DPP4FUN-COMPONENT-COMPONENT-NAME-FDA338", - ), - pytest.param( - "VALIDATION-DPP4FUN-COMPONENT-COMPONENT-REFERENCE-0E636A", - validate_component, - Component.model_construct(name="Leg", reference=" "), - "Component.reference", - id="VALIDATION-DPP4FUN-COMPONENT-COMPONENT-REFERENCE-0E636A", - ), - pytest.param( - "VALIDATION-DPP4FUN-PART-PART-NAME-B844F5", - validate_part, - Part.model_construct(name=""), - "Part.name", - id="VALIDATION-DPP4FUN-PART-PART-NAME-B844F5", - ), - pytest.param( - "VALIDATION-DPP4FUN-PART-PART-REFERENCE-98E9BF", - validate_part, - Part.model_construct(name="Foot", reference=" "), - "Part.reference", - id="VALIDATION-DPP4FUN-PART-PART-REFERENCE-98E9BF", - ), - ], -) -def test_dpp4fun_public_validators_reject_collection_and_component_errors( - contract_id: str, validator: Callable[[Any], None], value: Any, context: str -) -> None: - before = value.model_dump() if value is not None else None - with pytest.raises(DppValidationError, match=context): - validator(value) - if value is not None: - assert value.model_dump() == before + result = Dpp4FunValidationService().validate(passport) + assert not result.is_valid + assert result.issues[0].path == "header.digitalProductPassportId" + assert result.issues[-1].rule == "DPP4FUN_CONTENT_SPEC_SUPPORTED" + assert all(issue.severity is ValidationSeverity.ERROR for issue in result.issues) -@pytest.mark.parametrize( - ("contract_id", "validator", "value"), - [ - pytest.param( - "VALIDATION-CORE-VALIDATION-UTILS-VALIDATION-UTILS-B535AB", - validate_bill_of_materials, - BillOfMaterials(materials=(Material(name="Steel", portion=1.0),)), - id="VALIDATION-CORE-VALIDATION-UTILS-VALIDATION-UTILS-B535AB", - ), - pytest.param( - "VALIDATION-DPP4FUN-BILL-OF-MATERIALS-BILL-OF-MATERIALS-VALIDATOR-177D8A", - validate_bill_of_materials, - BillOfMaterials( - materials=(Material(name="Steel", portion=1.0),), - components=(Component(name="Leg", reference="L1"),), - parts=(Part(name="Foot", reference="F1"),), - ), - id="VALIDATION-DPP4FUN-BILL-OF-MATERIALS-BILL-OF-MATERIALS-VALIDATOR-177D8A", - ), - pytest.param( - "VALIDATION-DPP4FUN-DIMENSIONS-DIMENSIONS-VALIDATOR-E905DD", - validate_dimensions, - Dimensions(width=1.0, height=2.0, depth=3.0, unit="cm"), - id="VALIDATION-DPP4FUN-DIMENSIONS-DIMENSIONS-VALIDATOR-E905DD", - ), - pytest.param( - "VALIDATION-DPP4FUN-MATERIAL-MATERIAL-VALIDATOR-3525B6", - validate_material, - Material(name="Steel", portion=1.0), - id="VALIDATION-DPP4FUN-MATERIAL-MATERIAL-VALIDATOR-3525B6", - ), - ], -) -def test_dpp4fun_public_validators_accept_tuple_backed_values_repeatedly( - contract_id: str, validator: Callable[[Any], None], value: Any -) -> None: - before = value.model_dump() - validator(value) - validator(value) - assert value.model_dump() == before +def test_profile_validator_reports_null_as_ordered_semantic_result() -> None: + result = Dpp4FunValidator().validate(None) + assert not result.is_valid + assert [(item.rule, item.path, item.severity.value) for item in result.issues] == [ + ("DPP4FUN_REQUIRED", "", "ERROR") + ] -def test_dpp4fun_order_core_then_product_then_cross(valid_dpp4fun: Dpp4Fun) -> None: - invalid_core = valid_dpp4fun.with_updates( - coreDpp=valid_dpp4fun.coreDpp.with_updates( - passportMetadata=PassportMetadata.model_construct( - uniqueProductIdentifier=None, passportUpdateDates=() - ) +def test_optional_characteristics_scalars_are_not_required() -> None: + value = Dpp4Fun.create( + DppHeader( + digital_product_passport_id="urn:example:dpp:1", + unique_product_identifier="urn:example:product:1", + granularity=Granularity.MODEL, + dpp_schema_version="1.0", + dpp_status="active", + economic_operator_id="urn:example:operator:1", ) ) - with pytest.raises(DppValidationError, match="PassportMetadata"): - validate_dpp4fun(invalid_core) - invalid_product = valid_dpp4fun.with_updates( - classification=ProductClassification.model_construct(sector="", category="Chair", tags=()) - ) - with pytest.raises(DppValidationError, match="ProductClassification.sector"): - validate_dpp4fun(invalid_product) - invalid_cross_only = valid_dpp4fun.with_updates( - characteristics=valid_dpp4fun.characteristics.with_updates(productType="Table") - ) - with pytest.raises(DppValidationError, match="inconsistent"): - validate_dpp4fun(invalid_cross_only) - - -@pytest.mark.parametrize( - "contract_id", - [ - pytest.param( - "VALIDATION-DPP4FUN-CHARACTERISTICS-CHARACTERISTICS-VALIDATOR-CDD0A7", - id="VALIDATION-DPP4FUN-CHARACTERISTICS-CHARACTERISTICS-VALIDATOR-CDD0A7", - ), - pytest.param( - "VALIDATION-DPP4FUN-PRODUCT-CLASSIFICATION-PRODUCT-CLASSIFICATION-VALIDATOR-247411", - id="VALIDATION-DPP4FUN-PRODUCT-CLASSIFICATION-PRODUCT-CLASSIFICATION-VALIDATOR-247411", - ), - ], -) -def test_product_subobject_validators_accept_valid_subobjects_repeatedly( - contract_id: str, valid_dpp4fun: Dpp4Fun -) -> None: - value: Any - validator: Callable[[Any], None] - if contract_id.endswith("CDD0A7"): - value = valid_dpp4fun.characteristics - validator = validate_characteristics - else: - value = valid_dpp4fun.classification - validator = validate_product_classification - before = value.model_dump() - validator(value) - validator(value) - assert value.model_dump() == before - - -@pytest.mark.parametrize( - "contract_id", - [ - pytest.param( - "VALIDATION-DPP4FUN-DPP4-FUN-DPP4-FUN-VALIDATION-SERVICE-C8F833", - id="VALIDATION-DPP4FUN-DPP4-FUN-DPP4-FUN-VALIDATION-SERVICE-C8F833", - ), - pytest.param( - "VALIDATION-DPP4FUN-DPP4-FUN-DPP4-FUN-VALIDATOR-C84912", - id="VALIDATION-DPP4FUN-DPP4-FUN-DPP4-FUN-VALIDATOR-C84912", - ), - ], -) -def test_dpp4fun_public_validator_accepts_valid_dpp_repeatedly( - contract_id: str, valid_dpp4fun: Dpp4Fun -) -> None: - before = valid_dpp4fun.model_dump() - validate_dpp4fun(valid_dpp4fun) - validate_dpp4fun(valid_dpp4fun) - assert valid_dpp4fun.model_dump() == before + assert Dpp4FunValidator().validate(value).is_valid diff --git a/tests/test_end_to_end.py b/tests/test_end_to_end.py index 7193a28..161d684 100644 --- a/tests/test_end_to_end.py +++ b/tests/test_end_to_end.py @@ -1,59 +1,64 @@ -"""End-to-end: build -> validate -> serialize -> parse+validate -> identifiers.""" +"""Typed DPP4Fun build, validation, canonical transport, and generic client flow.""" from __future__ import annotations import json +from datetime import UTC, datetime import httpx -from dpp_sdk import ( - Dpp4Fun, - Dpp4FunJsonCodec, - from_json_and_validate, - to_json, - validate_dpp4fun, -) -from dpp_sdk.clients import DppRepoClient +from dpp_sdk import Dpp4Fun, Dpp4FunJsonCodec, DppHeader, Granularity, to_json, validate_dpp4fun +from dpp_sdk.clients import DppRepoClient, DppRepresentation +from dpp_sdk.dpp4fun import ChairCharacteristics, Nameplate -def test_full_happy_path(valid_dpp4fun: Dpp4Fun) -> None: - validate_dpp4fun(valid_dpp4fun) +def _passport() -> Dpp4Fun: + header = DppHeader( + digital_product_passport_id="11111111-1111-1111-1111-111111111111", + unique_product_identifier="GTIN-0001", + granularity=Granularity.MODEL, + dpp_schema_version="1.0", + dpp_status="active", + last_updated=datetime(2026, 1, 1, tzinfo=UTC), + economic_operator_id="operator-123", + ) + return Dpp4Fun.create( + header, + Nameplate.create(manufacturerName="ACME"), + ChairCharacteristics.create(color="blue"), + ) - wire = to_json(valid_dpp4fun) - restored = from_json_and_validate(wire) - assert restored == valid_dpp4fun - assert restored.dpp_id == "11111111-1111-1111-1111-111111111111" - assert restored.product_id == "GTIN-0001" - assert restored.passport_type == "Dpp4Fun Furniture" - assert restored.category == "Office Chair" - assert restored.productName == "ErgoChair Pro" +def test_full_happy_path() -> None: + value = _passport() + assert validate_dpp4fun(value).is_valid + wire = to_json(value) + restored = Dpp4FunJsonCodec().from_json_and_validate(wire) + assert restored.passport == value.passport + assert restored.header.digital_product_passport_id == "11111111-1111-1111-1111-111111111111" -def test_immutable_validated_model_round_trips_through_repository_client( - valid_dpp4fun: Dpp4Fun, -) -> None: - """The public domain, codec, and repository boundaries retain canonical values.""" - updated = valid_dpp4fun.with_updates( - classification=valid_dpp4fun.classification.with_updates(tags=("recyclable", "durable")) +def test_domain_mapping_stays_outside_generic_repository_client() -> None: + value = _passport() + nameplate = value.typed_aspect(Nameplate) + assert nameplate is not None + updated = value.with_updates( + aspects=(nameplate.with_field("manufacturerName", "ACME International"),) ) - validator_calls = 0 - - def validator(dpp: Dpp4Fun) -> None: - nonlocal validator_calls - validator_calls += 1 - validate_dpp4fun(dpp) + wire_value = json.loads(to_json(updated)) def handler(request: httpx.Request) -> httpx.Response: if request.method == "POST": assert request.url.raw_path == b"/v1/dpps" - assert json.loads(request.content)["classification"]["tags"] == [ - "recyclable", - "durable", - ] + assert "elements" in json.loads(request.content) return httpx.Response( 201, - json={"statusCode": "SuccessCreated", "payload": {"dppId": updated.dpp_id}}, + json={ + "statusCode": "SuccessCreated", + "payload": { + "digitalProductPassportId": updated.header.digital_product_passport_id + }, + }, ) if request.method == "GET": assert ( @@ -65,20 +70,15 @@ def handler(request: httpx.Request) -> httpx.Response: ) assert request.method == "PATCH" assert request.url.raw_path == b"/v1/dpps/DPP-1/elements/%24.characteristics.weight" - assert json.loads(request.content) is None + assert json.loads(request.content) == "updated" return httpx.Response(200, json={"statusCode": "Success", "payload": {"accepted": True}}) client = httpx.Client(transport=httpx.MockTransport(handler)) - repo = DppRepoClient( - "http://repo.test", - codec=Dpp4FunJsonCodec(), - validator=validator, - client=client, - ) - - assert updated.classification.tags == ("recyclable", "durable") - assert repo.create_dpp(updated).dppId == updated.dpp_id - assert repo.read_dpp_by_id(updated.dpp_id) == updated - assert repo.update_data_element("DPP-1", "$.characteristics.weight", None) == {"accepted": True} - assert validator_calls == 1 + repo = DppRepoClient("http://repo.test", client=client) + created = repo.create_dpp(wire_value) + assert created.digital_product_passport_id == updated.header.digital_product_passport_id + read = repo.read_dpp_by_id(updated.header.digital_product_passport_id, DppRepresentation.FULL) + assert Dpp4FunJsonCodec().from_json(json.dumps(read.payload)).passport == updated.passport + element = repo.update_data_element("DPP-1", "$.characteristics.weight", "updated") + assert element.payload == {"accepted": True} assert not client.is_closed diff --git a/tests/test_integration_live.py b/tests/test_integration_live.py index 65eb610..74fa3b6 100644 --- a/tests/test_integration_live.py +++ b/tests/test_integration_live.py @@ -1,102 +1,65 @@ -"""Opt-in live conformance tests for externally supplied repository endpoints. - -The suite auto-skips unless both configured endpoints answer ``GET /health``. -It covers create, reads, curated-element update, delete, and registry registration; -a fresh DPP identifier keeps the interaction idempotent. -""" +"""Opt-in live checks using neutral JSON and externally configured API URLs.""" from __future__ import annotations +import os from uuid import uuid4 import pytest -from dpp_sdk.clients import ( - DppRegistryClient, - DppRepoClient, - RegisterDppRequest, - local_repo_base_url, -) -from dpp_sdk.dpp4fun import Dpp4FunJsonCodec, validate_dpp4fun -from dpp_sdk.dpp4fun.model import Dpp4Fun +from dpp_sdk.clients import DppRegistryClient, DppRepoClient, RegisterDppRequest pytestmark = pytest.mark.integration -@pytest.fixture -def repo_client() -> DppRepoClient[Dpp4Fun]: - client: DppRepoClient[Dpp4Fun] = DppRepoClient.for_local_mock( - Dpp4FunJsonCodec(), validate_dpp4fun - ) - if not client.health_check(): - pytest.skip("repository endpoint is unavailable") - return client - - -@pytest.fixture -def registry_client() -> DppRegistryClient: - client = DppRegistryClient.for_local_mock() - if not client.health_check(): - pytest.skip("registry endpoint is unavailable") - return client - - -def _with_fresh_dpp_id(dpp: Dpp4Fun) -> Dpp4Fun: - """Return a copy with a new ``uniqueProductIdentifier`` (drives ``dpp_id``).""" - meta = dpp.coreDpp.passportMetadata.with_updates(uniqueProductIdentifier=uuid4()) - core = dpp.coreDpp.with_updates(passportMetadata=meta) - return dpp.with_updates(coreDpp=core) - - -def test_repo_lifecycle_against_external_endpoint( - repo_client: DppRepoClient[Dpp4Fun], valid_dpp4fun: Dpp4Fun -) -> None: - dpp = _with_fresh_dpp_id(valid_dpp4fun) - dpp_id = dpp.dpp_id - product_id = dpp.product_id - - created = repo_client.create_dpp(dpp) - assert created.dppId - - try: - assert repo_client.read_dpp_by_id(dpp_id).dpp_id == dpp_id - assert repo_client.read_dpp_by_product_id(product_id).product_id == product_id - - # Fine-granular read/update against the curated element path. - assert ( - repo_client.read_data_element(dpp_id, "$.characteristics.productName") - == dpp.productName - ) - updated = repo_client.update_data_element( - dpp_id, "$.characteristics.productName", "Updated via live test" - ) - assert updated == "Updated via live test" - - ids = repo_client.read_dpp_ids_by_product_ids([product_id], limit=10) - assert ids.dppIdentifiers is not None - assert dpp_id in ids.dppIdentifiers - finally: - repo_client.delete_dpp_by_id(dpp_id) - - -def test_registry_register_against_external_endpoint( - repo_client: DppRepoClient[Dpp4Fun], - registry_client: DppRegistryClient, - valid_dpp4fun: Dpp4Fun, -) -> None: - # The registry verifies the repo reference via HEAD, so the DPP must exist first. - dpp = _with_fresh_dpp_id(valid_dpp4fun) - dpp_id = dpp.dpp_id - repo_client.create_dpp(dpp) - try: - response = registry_client.post_new_dpp_to_registry( - RegisterDppRequest( - uniqueProductIdentifier=dpp.product_id, - digitalProductPassportId=dpp_id, - uniqueEconomicOperatorIdentifier="operator-123", - dppApiEndpoint=local_repo_base_url(), +def _base_url(name: str, default: str) -> str: + value = os.environ.get(name, default).strip() + if not value: + pytest.skip(f"{name} is empty") + return value + + +def _passport() -> dict[str, object]: + return { + "dpp_header": { + "digitalProductPassportId": str(uuid4()), + "uniqueProductIdentifier": f"urn:example:product:{uuid4()}", + "granularity": "item", + "dppSchemaVersion": "1.0", + "dppStatus": "active", + "lastUpdated": "2026-01-01T00:00:00Z", + "economicOperatorId": "urn:example:operator:1", + }, + "elements": [], + } + + +def test_repository_json_lifecycle_against_external_endpoint() -> None: + base = _base_url("DPP_REPO_BASE_URL", "http://localhost:8080") + passport = _passport() + identifier = passport["dpp_header"]["digitalProductPassportId"] # type: ignore[index] + with DppRepoClient(base) as repo: + created = repo.create_dpp(passport) + assert created.digital_product_passport_id + try: + read = repo.read_dpp_by_id(str(identifier)) + assert read.payload["dpp_header"]["digitalProductPassportId"] == identifier + ids = repo.read_dpp_ids_by_product_ids( + [passport["dpp_header"]["uniqueProductIdentifier"]] # type: ignore[index] ) - ) - assert response.registrationId - finally: - repo_client.delete_dpp_by_id(dpp_id) + assert str(identifier) in ids.dpp_identifiers + finally: + repo.delete_dpp_by_id(str(identifier)) + + +def test_registry_registration_against_external_endpoint() -> None: + registry_base = _base_url("DPP_REGISTRY_BASE_URL", "http://localhost:8081") + repo_base = _base_url("DPP_REPO_BASE_URL", "http://localhost:8080") + request = RegisterDppRequest( + unique_product_identifier="urn:example:product:1", + digital_product_passport_id="urn:example:dpp:1", + unique_economic_operator_identifier="urn:example:operator:1", + dpp_api_endpoint=repo_base, + ) + with DppRegistryClient(registry_base) as registry: + assert registry.post_new_dpp_to_registry(request).registration_id diff --git a/tests/test_java_contract_fixtures.py b/tests/test_java_contract_fixtures.py deleted file mode 100644 index aba4044..0000000 --- a/tests/test_java_contract_fixtures.py +++ /dev/null @@ -1,82 +0,0 @@ -"""Read-only conformance tests for Java Dpp4Fun fixtures. - -The metadata was generated at Java commit -``9933d674ba27cd987f1bba731eb57b8dbb6bba95`` by -``DeterministicFixtureGenerator``. Fixtures are test inputs only: production -transport never imports this directory. -""" - -from __future__ import annotations - -import hashlib -import json -from pathlib import Path - -import pytest -from pydantic import ValidationError - -from dpp_sdk.core.errors import DppMappingError, DppValidationError -from dpp_sdk.dpp4fun.transport import from_json, from_json_and_validate, to_json - -_FIXTURE_DIR = Path(__file__).resolve().parent / "fixtures" / "java-contract" -_METADATA = json.loads((_FIXTURE_DIR / "fixture-metadata.json").read_text(encoding="utf-8")) -_CONTRACT_IDS = { - "models:valid-complete-dpp": "FIXTURE-MODELS-VALID-COMPLETE-DPP", - "models:valid-minimal-dpp": "FIXTURE-MODELS-VALID-MINIMAL-DPP", - "models:valid-flat-input": "FIXTURE-MODELS-VALID-FLAT-INPUT", - "models:valid-nested-input": "FIXTURE-MODELS-VALID-NESTED-INPUT", - "models:canonical-java-output": "FIXTURE-MODELS-CANONICAL-JAVA-OUTPUT", - "errors:malformed-json": "FIXTURE-ERRORS-MALFORMED-JSON", - "errors:invalid-missing-required-field": "FIXTURE-ERRORS-INVALID-MISSING-REQUIRED-FIELD", - "errors:invalid-semantic-rule": "FIXTURE-ERRORS-INVALID-SEMANTIC-RULE", - "errors:invalid-cross-object-rule": "FIXTURE-ERRORS-INVALID-CROSS-OBJECT-RULE", -} - - -def test_fixture_directory_is_python_owned() -> None: - expected = Path(__file__).resolve().parent / "fixtures" / "java-contract" - assert _FIXTURE_DIR.resolve() == expected - assert (_FIXTURE_DIR / "fixture-metadata.json").is_file() - - -def _fixture_parameters() -> list[object]: - fixtures = _METADATA["fixtures"] - assert {entry["fixtureId"] for entry in fixtures} == set(_CONTRACT_IDS) - return [ - pytest.param(_CONTRACT_IDS[entry["fixtureId"]], entry, id=_CONTRACT_IDS[entry["fixtureId"]]) - for entry in fixtures - ] - - -@pytest.mark.parametrize(("contract_id", "entry"), _fixture_parameters()) -def test_java_fixture_hash_and_contract_outcome(contract_id: str, entry: dict[str, object]) -> None: - path = _FIXTURE_DIR / str(entry["file"]) - raw = path.read_text(encoding="utf-8") - assert hashlib.sha256(path.read_bytes()).hexdigest() == entry["pythonSnapshotSha256"] - - if entry["expectedOutcome"] == "success": - parsed = from_json_and_validate(raw) - if contract_id == "FIXTURE-MODELS-CANONICAL-JAVA-OUTPUT": - # JSON object ordering is non-normative; list order remains normative. - assert json.loads(to_json(parsed)) == json.loads(raw) - else: - assert from_json(to_json(parsed)) == parsed - return - - if entry["expectedException"] == "MappingException": - with pytest.raises(DppMappingError) as exc: - from_json(raw) - assert isinstance(exc.value.__cause__, ValidationError) - return - - if entry["expectedException"] == "IllegalArgumentException": - with pytest.raises(DppMappingError) as exc: - from_json(raw) - assert isinstance(exc.value.__cause__, json.JSONDecodeError) - return - - assert entry["expectedException"] == "ValidationException" - # Mapping succeeds; the explicit validated decoder retains the semantic category. - from_json(raw) - with pytest.raises(DppValidationError): - from_json_and_validate(raw) diff --git a/tests/test_live_test_gating.py b/tests/test_live_test_gating.py index f7974bc..67ed069 100644 --- a/tests/test_live_test_gating.py +++ b/tests/test_live_test_gating.py @@ -67,9 +67,12 @@ def do_POST(self) -> None: # noqa: N802 raw = self.rfile.read(length) if path == "/v1/dpps": self.server.dpp = json.loads(raw) - self.server.dpp_id = self.server.dpp["passportMetadata"]["uniqueProductIdentifier"] + self.server.dpp_id = self.server.dpp["dpp_header"]["digitalProductPassportId"] self._send_json( - {"statusCode": "SuccessCreated", "payload": {"dppId": self.server.dpp_id}} + { + "statusCode": "SuccessCreated", + "payload": {"digitalProductPassportId": self.server.dpp_id}, + } ) elif path == "/v1/dppsByProductIds": self._send_json( @@ -199,10 +202,10 @@ def test_explicit_opt_in_runs_root_live_tests_against_configured_alternate_ports ) assert result.returncode == 0, result.stdout + result.stderr - assert ("GET", "/health") in repository.records - assert ("GET", "/health") in registry.records assert ("POST", "/v1/dpps") in repository.records assert ("POST", "/v1/registerDPP") in registry.records + assert ("GET", "/health") not in repository.records + assert ("GET", "/health") not in registry.records def test_live_marker_remains_discoverable_and_controlled_client_tests_need_no_services() -> None: @@ -214,8 +217,8 @@ def test_live_marker_remains_discoverable_and_controlled_client_tests_need_no_se controlled_clients = _pytest(["tests/test_clients.py"], cwd=SDK_ROOT, env=_environment()) assert integration.returncode == 0, integration.stdout + integration.stderr - assert "test_repo_lifecycle_against_external_endpoint" in integration.stdout - assert "test_registry_register_against_external_endpoint" in integration.stdout + assert "test_repository_json_lifecycle_against_external_endpoint" in integration.stdout + assert "test_registry_registration_against_external_endpoint" in integration.stdout assert controlled_clients.returncode == 0, controlled_clients.stdout + controlled_clients.stderr diff --git a/tests/test_python_only_extensions.py b/tests/test_python_only_extensions.py index 20a9dfa..874dc85 100644 --- a/tests/test_python_only_extensions.py +++ b/tests/test_python_only_extensions.py @@ -1,180 +1,32 @@ -"""Regression coverage for supported Python-only client extensions.""" +"""Python-native synchronous HTTPX client ownership behavior.""" from __future__ import annotations -from collections.abc import Callable -from typing import Any - import httpx import pytest -from dpp_sdk.clients import ( - DEFAULT_REGISTRY_BASE_URL, - DEFAULT_REPO_BASE_URL, - DppRegistryClient, - DppRepoClient, - local_registry_base_url, - local_repo_base_url, -) +from dpp_sdk.clients import DppRegistryClient, DppRepoClient from dpp_sdk.clients._http import DEFAULT_CONNECT_TIMEOUT, DEFAULT_REQUEST_TIMEOUT -from dpp_sdk.dpp4fun.model import Dpp4Fun -from dpp_sdk.dpp4fun.transport import Dpp4FunJsonCodec -from dpp_sdk.dpp4fun.validation import validate_dpp4fun - - -def _repo(client: httpx.Client | None = None) -> DppRepoClient[Dpp4Fun]: - return DppRepoClient( - "http://repo.test", - Dpp4FunJsonCodec(), - validate_dpp4fun, - client=client, - ) - - -def _registry(client: httpx.Client | None = None) -> DppRegistryClient: - return DppRegistryClient("http://registry.test", client=client) -@pytest.mark.parametrize( - ("extension_id", "factory"), - [ - pytest.param( - "PYTHON-ONLY-CLIENT-LIFECYCLE-001-repo", - _repo, - id="PYTHON-ONLY-CLIENT-LIFECYCLE-001-repo", - ), - pytest.param( - "PYTHON-ONLY-CLIENT-LIFECYCLE-001-registry", - _registry, - id="PYTHON-ONLY-CLIENT-LIFECYCLE-001-registry", - ), - ], -) -def test_sdk_owned_clients_close_idempotently_and_support_context_managers( - extension_id: str, factory: Callable[[], Any] -) -> None: - sdk_client = factory() - assert sdk_client._client.timeout.connect == DEFAULT_CONNECT_TIMEOUT - assert sdk_client._client.timeout.read == DEFAULT_REQUEST_TIMEOUT - assert sdk_client._client.timeout.write == DEFAULT_REQUEST_TIMEOUT - assert sdk_client._client.timeout.pool == DEFAULT_REQUEST_TIMEOUT - - with sdk_client as entered: +@pytest.mark.parametrize("client_type", [DppRepoClient, DppRegistryClient]) +def test_owned_http_clients_close_idempotently(client_type: type[object]) -> None: + sdk_client = client_type("https://api.example") + internal = sdk_client._client # type: ignore[attr-defined] + assert internal.timeout.connect == DEFAULT_CONNECT_TIMEOUT + assert internal.timeout.read == DEFAULT_REQUEST_TIMEOUT + with sdk_client as entered: # type: ignore[attr-defined] assert entered is sdk_client - assert not sdk_client._client.is_closed - - assert sdk_client._client.is_closed - sdk_client.close() - assert sdk_client._client.is_closed + assert internal.is_closed + sdk_client.close() # type: ignore[attr-defined] -@pytest.mark.parametrize( - ("extension_id", "factory"), - [ - pytest.param( - "PYTHON-ONLY-CLIENT-LIFECYCLE-001-supplied-repo", - _repo, - id="PYTHON-ONLY-CLIENT-LIFECYCLE-001-supplied-repo", - ), - pytest.param( - "PYTHON-ONLY-CLIENT-LIFECYCLE-001-supplied-registry", - _registry, - id="PYTHON-ONLY-CLIENT-LIFECYCLE-001-supplied-registry", - ), - ], -) -def test_caller_supplied_clients_remain_open_after_close_and_context_exit( - extension_id: str, factory: Callable[[httpx.Client], Any] -) -> None: +@pytest.mark.parametrize("client_type", [DppRepoClient, DppRegistryClient]) +def test_injected_httpx_client_remains_caller_owned(client_type: type[object]) -> None: supplied = httpx.Client() - sdk_client = factory(supplied) - with sdk_client as entered: + sdk_client = client_type("https://api.example", client=supplied) + with sdk_client as entered: # type: ignore[attr-defined] assert entered is sdk_client - sdk_client.close() + sdk_client.close() # type: ignore[attr-defined] assert not supplied.is_closed supplied.close() - - -@pytest.mark.parametrize( - ("extension_id", "factory"), - [ - pytest.param( - "PYTHON-ONLY-REPO-HEALTH-001", - _repo, - id="PYTHON-ONLY-REPO-HEALTH-001", - ), - pytest.param( - "PYTHON-ONLY-REGISTRY-HEALTH-001", - _registry, - id="PYTHON-ONLY-REGISTRY-HEALTH-001", - ), - ], -) -@pytest.mark.parametrize( - ("outcome", "expected"), - [ - pytest.param(httpx.Response(204), True, id="2xx"), - pytest.param(httpx.Response(503), False, id="non-2xx"), - pytest.param(httpx.ReadTimeout("timeout"), False, id="timeout"), - pytest.param(httpx.ConnectError("transport"), False, id="transport"), - ], -) -def test_health_helpers_return_reachability_without_raising( - extension_id: str, - factory: Callable[[httpx.Client], Any], - outcome: httpx.Response | Exception, - expected: bool, -) -> None: - def handler(request: httpx.Request) -> httpx.Response: - assert request.method == "GET" - assert request.url.path == "/health" - if isinstance(outcome, Exception): - raise outcome - return outcome - - sdk_client = factory(httpx.Client(transport=httpx.MockTransport(handler))) - assert sdk_client.health_check() is expected - - -def test_local_endpoint_helpers_honor_defaults_ports_blank_values_and_url_precedence( - monkeypatch: pytest.MonkeyPatch, -) -> None: - for name in ( - "DPP_REPO_BASE_URL", - "DPP_REGISTRY_BASE_URL", - "DPP_REPO_PORT", - "DPP_REGISTRY_PORT", - ): - monkeypatch.delenv(name, raising=False) - assert local_repo_base_url() == DEFAULT_REPO_BASE_URL - assert local_registry_base_url() == DEFAULT_REGISTRY_BASE_URL - - monkeypatch.setenv("DPP_REPO_PORT", "9100") - monkeypatch.setenv("DPP_REGISTRY_PORT", "9101") - assert local_repo_base_url() == "http://localhost:9100" - assert local_registry_base_url() == "http://localhost:9101" - - monkeypatch.setenv("DPP_REPO_PORT", " ") - monkeypatch.setenv("DPP_REGISTRY_PORT", " ") - assert local_repo_base_url() == DEFAULT_REPO_BASE_URL - assert local_registry_base_url() == DEFAULT_REGISTRY_BASE_URL - - monkeypatch.setenv("DPP_REPO_BASE_URL", " http://repo.override ") - monkeypatch.setenv("DPP_REGISTRY_BASE_URL", "http://registry.override") - assert local_repo_base_url() == "http://repo.override" - assert local_registry_base_url() == "http://registry.override" - - -def test_local_factories_use_resolved_endpoint_without_starting_a_service( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setenv("DPP_REPO_BASE_URL", "http://repo.local") - monkeypatch.setenv("DPP_REGISTRY_BASE_URL", "http://registry.local") - repo = DppRepoClient.for_local_mock(Dpp4FunJsonCodec(), validate_dpp4fun) - registry = DppRegistryClient.for_local_mock() - try: - assert repo._base_url == "http://repo.local" - assert registry._base_url == "http://registry.local" - finally: - repo.close() - registry.close() diff --git a/tests/test_readme_examples.py b/tests/test_readme_examples.py index f02f96a..881e6dd 100644 --- a/tests/test_readme_examples.py +++ b/tests/test_readme_examples.py @@ -3,16 +3,22 @@ from __future__ import annotations import json +import re +from datetime import UTC, datetime, timedelta from pathlib import Path +from urllib.parse import unquote -from dpp_sdk import Dpp4Fun, from_json, to_json, validate_dpp4fun +import httpx + +from dpp_sdk import Dpp4Fun, DppHeader, Granularity, from_json, to_json, validate_dpp4fun from dpp_sdk.clients import ( RegisterDppRequest, RegisterDppResponse, - UpdateDataElementRequest, ) +from dpp_sdk.dpp4fun import Nameplate README = Path(__file__).parents[1] / "README.md" +LIFECYCLE_EXAMPLE = README.parent / "examples" / "consumer_lifecycle.py" MODULE_READMES = { "core": README.parent / "src" / "dpp_sdk" / "core" / "README.md", "dpp4fun": README.parent / "src" / "dpp_sdk" / "dpp4fun" / "README.md", @@ -22,98 +28,272 @@ def test_readme_documents_current_model_and_client_contracts() -> None: readme = README.read_text(encoding="utf-8") + normalized_readme = re.sub(r"\s+", " ", readme) for required_text in ( - "fail-fast", - "with_updates", - "immutable tuples", - "POST /v1/registerDPP", - "read_compressed_dpp_by_id", - "read_dpp_version_by_id_and_date", - "legacy compatibility only", - "DppMappingClientError", - "caller-owned", - "UpdateDataElementRequest", - "## Standards alignment", + "DppRepresentation", + "## Standards and limitations", "EN 18222:2026", - "not a formal compliance implementation", - "## Known standards limitations", - "project-defined", - "complete generic implementation of RFC 7396 JSON Merge Patch", - "complete implementation of RFC 9535 JSONPath", - "does not record lifecycle events", - "dpp-sdk==0.4.0", - "dpp-sdk~=0.4.0", - "Install this local checkout", - "Create the development environment", - 'pip install -e ".[dev,release]"', - "must not be committed", - "pip version\nselectors omit the `v`", + "not a claim of full conformance", + "JSON Merge Patch (RFC 7396)", + "JSONPath (RFC 9535)", + "no lifecycle-event history", + "pip install dpp-sdk", ): - assert required_text in readme - assert 'dppApiEndpoint="https://repo.example.com"' in readme - assert "dppApiEndpoint=local_repo_base_url()" not in readme - - -def test_readme_model_example_uses_immutable_updates_and_json_arrays( - valid_dpp4fun: Dpp4Fun, -) -> None: - # README: validate explicitly, update immutably, then use the normal codec. - validate_dpp4fun(valid_dpp4fun) - updated = valid_dpp4fun.with_updates( - characteristics=valid_dpp4fun.characteristics.with_updates(productName="ErgoChair Pro 2") - ) - validate_dpp4fun(updated) + assert required_text in normalized_readme + contributing = (README.parent / "CONTRIBUTING.md").read_text(encoding="utf-8") + assert 'pip install -e ".[dev,release]"' in contributing + assert "dpp-sdk==0.4.0" not in readme + assert "dpp-sdk~=0.4.0" not in readme + model = (README.parent / "docs/model-guide.md").read_text(encoding="utf-8") + clients = (README.parent / "docs/clients-guide.md").read_text(encoding="utf-8") + assert "ordered" in model and "immutable" in model + for detail in ("POST /v1/registerDPP", "DppMappingClientError", "caller owned"): + assert detail in clients + assert 'dpp_api_endpoint=f"{REPO_URL}/v1/dpps"' in readme + assert "dpp_api_endpoint=local_repo_base_url()" not in readme + lifecycle = LIFECYCLE_EXAMPLE.read_text(encoding="utf-8") + assert "with_updates" in lifecycle + assert "read_dpp_version_by_id_and_date" in lifecycle - assert updated is not valid_dpp4fun - assert valid_dpp4fun.characteristics.productName == "ErgoChair Pro" - assert isinstance(updated.classification.tags, tuple) - assert json.loads(to_json(updated))["classification"]["tags"] == ["ergonomic", "adjustable"] + +def test_readme_model_example_uses_immutable_updates_and_canonical_json() -> None: + header = DppHeader( + digital_product_passport_id="urn:example:dpp:1", + unique_product_identifier="urn:example:product:1", + granularity=Granularity.MODEL, + dpp_schema_version="1.0", + dpp_status="active", + last_updated=datetime(2026, 1, 1, tzinfo=UTC), + economic_operator_id="urn:example:operator:1", + ) + passport = Dpp4Fun.create(header, Nameplate.create(manufacturerName="Example")) + assert validate_dpp4fun(passport).is_valid + nameplate = passport.typed_aspect(Nameplate) + assert nameplate is not None + updated = passport.with_updates( + aspects=(nameplate.with_field("manufacturerName", "Example 2"),) + ) + assert validate_dpp4fun(updated).is_valid + assert updated is not passport + assert json.loads(to_json(updated))["elements"][0]["elements"][0]["value"] == "Example 2" assert from_json(to_json(updated)) == updated def test_readme_registry_example_emits_canonical_names_without_network() -> None: request = RegisterDppRequest( - uniqueProductIdentifier="GTIN-0001", - digitalProductPassportId="DPP-1", - uniqueEconomicOperatorIdentifier="operator-123", - dppApiEndpoint="https://repo.example.com", + unique_product_identifier="GTIN-0001", + digital_product_passport_id="DPP-1", + unique_economic_operator_identifier="operator-123", + dpp_api_endpoint="https://repo.example.com", ) - response = RegisterDppResponse(registrationId="REG-1") + response = RegisterDppResponse(registration_id="REG-1") - assert request.model_dump() == { + assert request.model_dump(by_alias=True) == { "uniqueProductIdentifier": "GTIN-0001", "digitalProductPassportId": "DPP-1", "uniqueEconomicOperatorIdentifier": "operator-123", "dppApiEndpoint": "https://repo.example.com", } - assert response.registrationId == "REG-1" - assert UpdateDataElementRequest(payload=None).payload is None + assert response.registration_id == "REG-1" -def test_module_readmes_link_to_the_single_release_owner_and_keep_focused_checks() -> None: +def test_module_readmes_point_to_the_guides_instead_of_repeating_them() -> None: required_text = { - "core": ( - "# DPP SDK core module", - "validate_dpp_core()", - "[release guide](../../../RELEASING.md)", - "tests/test_core_model.py tests/test_core_validation.py", - ), + "core": ("# DPP SDK core module", "DppValidator", "../../../docs/model-guide.md"), "dpp4fun": ( "# DPP SDK DPP4Fun module", "Dpp4FunJsonCodec", - "[release guide](../../../RELEASING.md)", - "tests/test_dpp4fun_validation.py tests/test_transport_roundtrip.py", - ), - "clients": ( - "# DPP SDK clients module", - "DppRepoClient[T]", - "[RELEASING.md](../../../RELEASING.md)", - "tests/test_clients.py tests/test_end_to_end.py", + "../../../docs/dpp4fun-fields.md", ), + "clients": ("# DPP SDK clients module", "DppRepoClient", "../../../docs/clients-guide.md"), } for module, expected in required_text.items(): content = MODULE_READMES[module].read_text(encoding="utf-8") for text in expected: assert text in content - assert "python -m build" not in content + assert "[Contributing](../../../CONTRIBUTING.md)" in content + assert "```" not in content + assert "![" not in content # images break once the README ships outside the repository + + +def test_readme_workflow_runs_with_mocked_repository_and_registry(monkeypatch, capsys) -> None: + blocks = re.findall(r"(?ms)^```python\s*\n(.*?)^```\s*$", README.read_text(encoding="utf-8")) + calls: list[tuple[str, str, dict[str, object]]] = [] + stored: dict[str, object] = {} + + def handler(request: httpx.Request) -> httpx.Response: + body = json.loads(request.content) if request.content else {} + calls.append((request.method, request.url.path, body)) + if request.method == "GET": + assert request.url.params["representation"] == "full" + return httpx.Response(200, json={"statusCode": "Success", "payload": stored}) + if request.url.path == "/v1/dpps": + stored.update(body) + assert request.url.params["representation"] == "full" + return httpx.Response( + 201, + json={ + "statusCode": "SuccessCreated", + "payload": {"digitalProductPassportId": body["digitalProductPassportId"]}, + }, + ) + assert request.url.path == "/v1/registerDPP" + return httpx.Response( + 201, json={"statusCode": "SuccessCreated", "payload": {"registrationId": "REG-1"}} + ) + + import dpp_sdk.clients._http as client_http + + monkeypatch.setattr( + client_http, "build_client", lambda: httpx.Client(transport=httpx.MockTransport(handler)) + ) + exec(compile(blocks[0], str(README), "exec"), {}) + + assert [(method, path) for method, path, _ in calls] == [ + ("POST", "/v1/dpps"), + ("POST", "/v1/registerDPP"), + ("GET", f"/v1/dpps/{calls[0][2]['digitalProductPassportId']}"), + ] + passport_id = calls[0][2]["digitalProductPassportId"] + assert calls[1][2]["digitalProductPassportId"] == passport_id + assert calls[1][2]["dppApiEndpoint"] == "http://localhost:8080/v1/dpps" + output = capsys.readouterr().out + assert f"Saved passport: {passport_id}" in output + assert "Registered: REG-1" in output + assert "Manufacturer: Example Furniture" in output + + +def test_lifecycle_example_runs_with_mocked_repository_and_registry(monkeypatch) -> None: + source = LIFECYCLE_EXAMPLE.read_text(encoding="utf-8") + state: dict[str, object] = { + "initial": None, + "stored": None, + "created_at": None, + "updated_at": None, + "history_at": None, + "calls": [], + } + + def repository_handler(request: httpx.Request) -> httpx.Response: + path = unquote(request.url.path) + calls = state["calls"] + assert isinstance(calls, list) + calls.append((request.method, path)) + if request.method == "POST" and path == "/v1/dpps": + stored = json.loads(request.content) + state["initial"] = stored + state["stored"] = stored + state["created_at"] = datetime.now(UTC) + return httpx.Response( + 201, + json={ + "statusCode": "SuccessCreated", + "payload": {"digitalProductPassportId": stored["digitalProductPassportId"]}, + }, + ) + if request.method == "PATCH" and path.startswith("/v1/dpps/"): + stored = state["stored"] + assert isinstance(stored, dict) + updated = {**stored, **json.loads(request.content)} + state["stored"] = updated + state["updated_at"] = datetime.now(UTC) + return httpx.Response(200, json={"statusCode": "Success", "payload": updated}) + if request.method == "GET" and path.startswith("/v1/dppsByIdAndDate/"): + at = datetime.fromisoformat(request.url.params["date"].replace("Z", "+00:00")) + assert at.utcoffset() is not None + state["history_at"] = at + created_at = state["created_at"] + updated_at = state["updated_at"] + assert isinstance(created_at, datetime) + if at < created_at: + return httpx.Response( + 404, + json={ + "statusCode": "ClientErrorResourceNotFound", + "message": [ + {"messageType": "Error", "text": "No DPP version at this date"} + ], + }, + ) + if isinstance(updated_at, datetime) and at >= updated_at: + return httpx.Response( + 200, json={"statusCode": "Success", "payload": state["stored"]} + ) + return httpx.Response(200, json={"statusCode": "Success", "payload": state["initial"]}) + if request.method == "GET" and path.startswith("/v1/dpps/"): + return httpx.Response(200, json={"statusCode": "Success", "payload": state["stored"]}) + if request.method == "DELETE" and path.startswith("/v1/dpps/"): + return httpx.Response(204) + raise AssertionError(f"unexpected request: {request.method} {path}") + + registry_calls: list[str] = [] + + def registry_handler(request: httpx.Request) -> httpx.Response: + registry_calls.append(unquote(request.url.path)) + assert request.method == "POST" and request.url.path == "/v1/registerDPP" + assert json.loads(request.content)["dppApiEndpoint"] == "http://repository.test/v1/dpps" + return httpx.Response( + 201, json={"statusCode": "SuccessCreated", "payload": {"registrationId": "REG-1"}} + ) + + import dpp_sdk.clients._http as client_http + + clients = iter( + ( + httpx.Client(transport=httpx.MockTransport(repository_handler)), + httpx.Client(transport=httpx.MockTransport(registry_handler)), + ) + ) + monkeypatch.setattr(client_http, "build_client", lambda: next(clients)) + namespace: dict[str, object] = {} + exec(compile(source, str(LIFECYCLE_EXAMPLE), "exec"), namespace) + workflow = namespace["run_consumer_workflow"] + assert callable(workflow) + result = workflow("http://repository.test", "http://registry.test") + assert result["updated_manufacturer"] == "Example Furniture Ltd." + historical = result["historical_passport"].typed_aspect(Nameplate) + assert historical is not None + assert historical.field("manufacturerName").value == "Example Furniture" + assert result["registration_id"] == "REG-1" + assert result["repository_cleanup_status"].value == "SuccessNoContent" + assert len(json.loads(result["full_json"])["elements"]) == 2 + assert json.loads(result["compressed_json"]) + assert registry_calls == ["/v1/registerDPP"] + assert [method for method, _ in state["calls"]] == [ + "POST", + "GET", + "PATCH", + "GET", + "GET", + "DELETE", + ] + created_at = state["created_at"] + updated_at = state["updated_at"] + history_at = state["history_at"] + assert isinstance(created_at, datetime) + assert isinstance(updated_at, datetime) + assert isinstance(history_at, datetime) + content_updated_at = result["historical_passport"].header.last_updated + assert content_updated_at is not None + assert content_updated_at < created_at + assert created_at <= history_at < updated_at + passport_id = result["passport_id"] + + def historical_response(at: datetime) -> httpx.Response: + return repository_handler( + httpx.Request( + "GET", + f"http://repository.test/v1/dppsByIdAndDate/{passport_id}", + params={"date": at.isoformat()}, + ) + ) + + assert historical_response(created_at - timedelta(microseconds=1)).status_code == 404 + assert historical_response(content_updated_at).status_code == 404 + assert historical_response(created_at).json()["payload"] == state["initial"] + assert ( + historical_response(updated_at - timedelta(microseconds=1)).json()["payload"] + == state["initial"] + ) + assert historical_response(updated_at).json()["payload"] == state["stored"] diff --git a/tests/test_release_validation.py b/tests/test_release_validation.py index 3e15b1c..360abb4 100644 --- a/tests/test_release_validation.py +++ b/tests/test_release_validation.py @@ -50,6 +50,8 @@ def test_archive_member_policy_rejects_local_environment_and_internal_paths() -> allowed = ( f"dpp_sdk-{_current_package_version()}/README.md", + f"dpp_sdk-{_current_package_version()}/docs/model-guide.md", + f"dpp_sdk-{_current_package_version()}/docs/clients-guide.md", f"dpp_sdk-{_current_package_version()}/LICENSE", f"dpp_sdk-{_current_package_version()}/src/dpp_sdk/py.typed", f"dpp_sdk-{_current_package_version()}/tests/test_clients.py", @@ -71,12 +73,16 @@ def test_root_sdist_excludes_local_environment_and_generated_output(tmp_path: Pa archive = next(output.glob("dpp_sdk-*.tar.gz")) checks = _release_checks() with tarfile.open(archive) as source_distribution: + names = {member.name for member in source_distribution.getmembers()} forbidden = [ member.name for member in source_distribution.getmembers() if checks.is_forbidden_archive_member(member.name) ] assert forbidden == [] + root = f"dpp_sdk-{_current_package_version()}/docs/" + assert root + "model-guide.md" in names + assert root + "clients-guide.md" in names def _build_sdist(checkout: Path, output: Path) -> Path: @@ -98,10 +104,28 @@ def _archive_member_names(archive: Path) -> list[str]: def test_root_sdist_excludes_fake_environment_from_copied_checkout(tmp_path: Path) -> None: checkout = tmp_path / "checkout" + + def ignore_workspace_site(directory: str, names: list[str]) -> set[str]: + return set( + shutil.ignore_patterns( + ".git", + ".codex", + ".venv", + "*-venv", + "__pycache__", + ".pytest_cache", + ".mypy_cache", + ".ruff_cache", + "build", + "dist", + "artifacts", + )(directory, names) + ) + shutil.copytree( ROOT, checkout, - ignore=shutil.ignore_patterns(".git", ".venv", "__pycache__", ".pytest_cache"), + ignore=ignore_workspace_site, ) environment = checkout / "src" / "dpp_sdk" / "archive-test-venv" metadata = environment / "Lib" / "site-packages" / "foreign_package-1.0.dist-info" / "METADATA" @@ -159,9 +183,11 @@ def test_package_version_cli_reports_the_current_package_version( assert capsys.readouterr().out.strip() == _current_package_version() -def test_0_4_0_release_surface_keeps_exact_sdk_consumers_in_sync() -> None: +def test_0_5_0_release_surface_keeps_exact_sdk_consumers_in_sync() -> None: + package_version = _current_package_version() package = (ROOT / "src" / "dpp_sdk" / "__init__.py").read_text(encoding="utf-8") changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8") + project = (ROOT / "pyproject.toml").read_text(encoding="utf-8") root_readme = (ROOT / "README.md").read_text(encoding="utf-8") demo_pyproject = (ROOT / "examples" / "mock-services-demo" / "pyproject.toml").read_text( encoding="utf-8" @@ -171,16 +197,31 @@ def test_0_4_0_release_surface_keeps_exact_sdk_consumers_in_sync() -> None: ) workflow = MOCK_SERVICES_WORKFLOW.read_text(encoding="utf-8") - assert '__version__ = "0.4.0"' in package + assert package_version == "0.5.0" + assert f'__version__ = "{package_version}"' in package + assert "## [Unreleased]" in changelog assert "## [0.4.0]" in changelog - assert "dpp-sdk==0.4.0" in root_readme - assert "dpp-sdk~=0.4.0" in root_readme - assert 'dependencies = ["dpp-sdk==0.4.0", "httpx>=0.27"]' in demo_pyproject + assert "## [0.5.0]" in changelog + assert "Breaking changes and migration" in changelog + assert "DppRepoClient(base_url, codec, validate_dpp4fun)" in changelog + assert "if not validation.is_valid" in changelog + assert "passport.dpp_header.unique_product_identifier" in changelog + assert "pip install dpp-sdk" in root_readme + assert package_version not in root_readme + assert "release candidate" not in root_readme.lower() + assert "PyPI currently lists" not in root_readme + assert "unpublished" not in changelog.lower() + assert "release candidate" not in changelog.lower() + assert f'dependencies = ["dpp-sdk=={package_version}", "httpx>=0.27"]' in demo_pyproject assert "dpp-sdk==0.4.0" not in demo_readme - assert "dpp_sdk-0.4.0-py3-none-any.whl" in workflow + assert "manifest unknown" not in demo_readme.lower() + assert "remains unverified" not in demo_readme.lower() + assert f"dpp_sdk-{package_version}-py3-none-any.whl" in workflow assert "default: 0.5.1" in workflow assert " - 0.5.1" in workflow assert "env/0.5.1.env" in workflow + assert 'Homepage = "https://github.com/CIR4FUN-EU/python-dpp-sdk"' in project + assert 'Issues = "https://github.com/CIR4FUN-EU/python-dpp-sdk/issues"' in project def test_release_workflow_requires_all_quality_gates_before_publish() -> None: diff --git a/tests/test_smoke.py b/tests/test_smoke.py index ca7ba01..3e2c89b 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -1,113 +1,51 @@ -"""Smoke test confirming the package imports and the scaffold is wired correctly. - -Replace/extend as real modules land. The full suite should mirror the Java tests in -the maintained SDK contract (round-trip, edge cases, inbound validation, end-to-end scenarios). -""" - -import pytest +"""Public package imports and intentional API boundary checks.""" import dpp_sdk import dpp_sdk.clients as clients import dpp_sdk.core as core import dpp_sdk.dpp4fun as dpp4fun -from dpp_sdk.clients import ( - CreateDppResponse, - DeleteDppResponse, - DppCodec, - DppRegistryClient, - DppRepoClient, - DppValidator, - ReadDppIdsRequest, - ReadDppIdsResponse, - RegisterDppRequest, - RegisterDppResponse, - UpdateDataElementRequest, -) -from dpp_sdk.core import ( - Address, - Contact, - Documentation, - Dpp, - DppCore, - Email, - Nameplate, - Organization, - OrganizationRole, - PassportMetadata, - Telephone, -) -from dpp_sdk.dpp4fun import ( - BillOfMaterials, - Characteristics, - Component, - Dimensions, - Dpp4Fun, - Material, - Part, - ProductClassification, -) def test_package_version() -> None: assert isinstance(dpp_sdk.__version__, str) - assert dpp_sdk.__version__.count(".") >= 2 assert "__version__" in dpp_sdk.__all__ -@pytest.mark.parametrize( - "contract_id", - ["EXPORT-PUBLIC-CLIENTS-001", "EXPORT-PUBLIC-CORE-001", "EXPORT-PUBLIC-DPP4FUN-001"], -) -def test_curated_exports_resolve_from_their_owning_packages(contract_id: str) -> None: - client_exports = { - "CreateDppResponse": CreateDppResponse, - "DeleteDppResponse": DeleteDppResponse, - "DppCodec": DppCodec, - "DppRegistryClient": DppRegistryClient, - "DppRepoClient": DppRepoClient, - "DppValidator": DppValidator, - "ReadDppIdsRequest": ReadDppIdsRequest, - "ReadDppIdsResponse": ReadDppIdsResponse, - "RegisterDppRequest": RegisterDppRequest, - "RegisterDppResponse": RegisterDppResponse, - "UpdateDataElementRequest": UpdateDataElementRequest, - } - core_exports = { - "Address": Address, - "Contact": Contact, - "Documentation": Documentation, - "Dpp": Dpp, - "DppCore": DppCore, - "Email": Email, - "Nameplate": Nameplate, - "Organization": Organization, - "OrganizationRole": OrganizationRole, - "PassportMetadata": PassportMetadata, - "Telephone": Telephone, - } - dpp4fun_exports = { - "BillOfMaterials": BillOfMaterials, - "Characteristics": Characteristics, - "Component": Component, - "Dimensions": Dimensions, - "Dpp4Fun": Dpp4Fun, - "Material": Material, - "Part": Part, - "ProductClassification": ProductClassification, - } - - for name, symbol in client_exports.items(): - assert getattr(clients, name) is symbol - assert name in clients.__all__ - for name, symbol in core_exports.items(): - assert getattr(core, name) is symbol - assert name in core.__all__ - for name, symbol in dpp4fun_exports.items(): - assert getattr(dpp4fun, name) is symbol +def test_all_declared_exports_resolve_and_match_their_packages() -> None: + for package in (dpp_sdk, clients, core, dpp4fun): + assert len(package.__all__) == len(set(package.__all__)) + assert all(hasattr(package, name) for name in package.__all__) + + for name in clients.__all__: + assert hasattr(clients, name) + for name in core.__all__: + assert hasattr(core, name) + for name in dpp4fun.__all__: + assert hasattr(dpp4fun, name) + for name in ( + "Dpp4FunElementDefinition", + "Dpp4FunQuantity", + "Dpp4FunSemanticProfile", + ): assert name in dpp4fun.__all__ - assert dpp_sdk.Dpp4Fun is Dpp4Fun - assert not hasattr(dpp_sdk, "DppRepoClient") + +def test_obsolete_compatibility_apis_are_absent() -> None: + for package in (dpp_sdk, core): + for name in ("Dpp", "DppCore", "PassportMetadata", "validate_dpp_core"): + assert not hasattr(package, name) + for name in ( + "DppCodec", + "DppValidator", + "DppValidationClientError", + "UpdateDataElementRequest", + "local_repo_base_url", + "local_registry_base_url", + ): + assert not hasattr(clients, name) + assert not hasattr(dpp4fun, "ProductClassification") + for name in ("ProfileDefinition", "SemanticProfile", "SemanticQuantity"): + assert not hasattr(dpp4fun, name) def test_subpackages_importable() -> None: diff --git a/tests/test_transport_roundtrip.py b/tests/test_transport_roundtrip.py index 6e89038..3d76bad 100644 --- a/tests/test_transport_roundtrip.py +++ b/tests/test_transport_roundtrip.py @@ -1,166 +1,170 @@ -"""JSON transport: flat outbound, flat-or-nested inbound, and edge cases.""" +"""Canonical DPP4Fun transport contracts over the shared core codec.""" from __future__ import annotations import json +from datetime import UTC, datetime import pytest -from pydantic import ValidationError -from dpp_sdk.core.errors import DppMappingError -from dpp_sdk.dpp4fun.model import Dpp4Fun -from dpp_sdk.dpp4fun.transport import from_json, from_json_and_validate, to_json - -_CORE_KEYS = {"passportMetadata", "nameplate", "documentation"} - - -def test_outbound_is_flattened(valid_dpp4fun: Dpp4Fun) -> None: - data = json.loads(to_json(valid_dpp4fun)) - assert "coreDpp" not in data - assert data.keys() >= _CORE_KEYS - assert {"classification", "characteristics", "billOfMaterials"} <= data.keys() - - -def test_outbound_key_order_matches_java(valid_dpp4fun: Dpp4Fun) -> None: - keys = list(json.loads(to_json(valid_dpp4fun)).keys()) - assert keys == [ - "classification", - "characteristics", - "billOfMaterials", - "passportMetadata", - "nameplate", - "documentation", - ] - - -def test_round_trip_from_flat(valid_dpp4fun: Dpp4Fun) -> None: - assert from_json(to_json(valid_dpp4fun)) == valid_dpp4fun - - -def test_round_trip_from_nested(valid_dpp4fun: Dpp4Fun) -> None: - nested = json.dumps(valid_dpp4fun.model_dump(mode="json")) - assert from_json(nested) == valid_dpp4fun - - -def test_nested_takes_precedence_over_duplicate_flat_keys(valid_dpp4fun: Dpp4Fun) -> None: - nested = valid_dpp4fun.model_dump(mode="json") - # Add a stray flat key alongside coreDpp; normalize should drop it. - nested["nameplate"] = {"gtinCode": "SHOULD-BE-DROPPED"} - parsed = from_json(json.dumps(nested)) - assert parsed.gtinCode == valid_dpp4fun.gtinCode - - -def test_empty_lists_serialized_not_null(valid_dpp4fun: Dpp4Fun) -> None: - minimal = valid_dpp4fun.with_updates( - classification=valid_dpp4fun.classification.with_updates(tags=[]) - ) - data = json.loads(to_json(minimal)) - assert data["classification"]["tags"] == [] - assert data["characteristics"]["features"] == ["lumbar-support"] - - -def test_from_json_and_validate(valid_dpp4fun: Dpp4Fun) -> None: - assert from_json_and_validate(to_json(valid_dpp4fun)) == valid_dpp4fun - - -def test_codec_is_usable_as_dpp_codec(valid_dpp4fun: Dpp4Fun) -> None: - from dpp_sdk.dpp4fun.transport import Dpp4FunJsonCodec - - codec = Dpp4FunJsonCodec() - assert codec.from_json(codec.to_json(valid_dpp4fun)) == valid_dpp4fun - - -@pytest.mark.parametrize( - "contract_id", - [pytest.param("CODEC-MALFORMED-JSON-001", id="CODEC-MALFORMED-JSON-001")], +from dpp_sdk.core import ( + DataElementCollection, + DppHeader, + Granularity, + SingleValuedDataElement, ) -def test_malformed_json_is_mapping_error_with_syntax_cause(contract_id: str) -> None: - with pytest.raises(DppMappingError) as exc: - from_json("{not-json") - assert isinstance(exc.value.__cause__, json.JSONDecodeError) - - -@pytest.mark.parametrize( - "contract_id", - [pytest.param("MAPPING-DOMAIN-PAYLOAD-001", id="MAPPING-DOMAIN-PAYLOAD-001")], +from dpp_sdk.core.json_codec import DppJsonException +from dpp_sdk.dpp4fun import Dpp4Fun, Dpp4FunJsonCodec, Nameplate +from dpp_sdk.dpp4fun.characteristics import ( + ChairCharacteristics, + CribCharacteristics, + Dpp4FunProductVariant, + FabricCharacteristics, + LockerCharacteristics, + ParticleBoardCharacteristics, + SofaCharacteristics, ) -def test_structurally_unmappable_json_is_mapping_error_with_pydantic_cause( - contract_id: str, -) -> None: - with pytest.raises(DppMappingError) as exc: - from_json('{"classification": {}}') - assert isinstance(exc.value.__cause__, ValidationError) +from dpp_sdk.dpp4fun.profile import DPP4FUN_PROFILE +from dpp_sdk.dpp4fun.validation import Dpp4FunValidationService + + +def _value() -> Dpp4Fun: + header = DppHeader( + digital_product_passport_id="urn:example:dpp:1", + unique_product_identifier="urn:example:product:1", + granularity=Granularity.MODEL, + dpp_schema_version="1.0", + dpp_status="active", + last_updated=datetime(2026, 1, 1, tzinfo=UTC), + economic_operator_id="urn:example:operator:1", + ) + return Dpp4Fun.create(header, Nameplate.create(manufacturerName="Example")) -@pytest.mark.parametrize( - ("raw", "cause_type"), - [ - pytest.param("null", ValidationError, id="null"), - pytest.param("", json.JSONDecodeError, id="missing-root"), - pytest.param("{}", ValidationError, id="empty-object"), - ], -) -def test_dec_004_standalone_invalid_roots_are_causal_mapping_failures( - raw: str, - cause_type: type[Exception], -) -> None: - with pytest.raises(DppMappingError) as exc: - from_json(raw) - - assert isinstance(exc.value.__cause__, cause_type) +def test_full_and_compressed_round_trip() -> None: + codec = Dpp4FunJsonCodec() + value = _value() + expanded = codec.to_expanded_json(value) + compressed = codec.to_compressed_json(value) + assert codec.from_expanded_json(expanded).passport == value.passport + assert codec.from_compressed_json(compressed).passport == value.passport @pytest.mark.parametrize( - ("field", "members", "path"), + ("variant", "aspect_type", "surface_text"), [ - pytest.param("features", [None, "valid"], "characteristics.features.0", id="null-first"), - pytest.param("tags", ["valid", None], "classification.tags.1", id="null-later"), + (Dpp4FunProductVariant.CHAIR, ChairCharacteristics, "Smooth"), + (Dpp4FunProductVariant.CRIB, CribCharacteristics, "Smooth semi-matte"), + (Dpp4FunProductVariant.FABRIC, FabricCharacteristics, "Woven matte"), + (Dpp4FunProductVariant.LOCKER, LockerCharacteristics, "Impregnated decor paper"), + ( + Dpp4FunProductVariant.PARTICLE_BOARD, + ParticleBoardCharacteristics, + "Small-structured chenille fabric", + ), + ( + Dpp4FunProductVariant.SOFA, + SofaCharacteristics, + "Small-structured chenille fabric, micro-textured metal", + ), ], ) -def test_dec_001_null_string_list_member_is_causal_mapping_failure( - valid_dpp4fun: Dpp4Fun, - field: str, - members: list[str | None], - path: str, +def test_published_surface_strings_round_trip_through_full_and_compressed( + variant, aspect_type, surface_text ) -> None: - payload = json.loads(to_json(valid_dpp4fun)) - owner = "characteristics" if field == "features" else "classification" - payload[owner][field] = members - - with pytest.raises(DppMappingError) as exc: - from_json(json.dumps(payload)) - - assert isinstance(exc.value.__cause__, ValidationError) - assert path in str(exc.value.__cause__) - - -@pytest.mark.parametrize("token", ["NaN", "Infinity", "-Infinity", "1e309"]) -def test_dec_003_non_finite_json_input_is_causal_mapping_failure( - valid_dpp4fun: Dpp4Fun, - token: str, -) -> None: - payload = to_json(valid_dpp4fun).replace('"weight": 14.5', f'"weight": {token}') - - with pytest.raises(DppMappingError) as exc: - from_json(payload) - - assert exc.value.__cause__ is not None - - -def test_dec_003_finite_exponent_json_is_accepted(valid_dpp4fun: Dpp4Fun) -> None: - payload = to_json(valid_dpp4fun).replace('"weight": 14.5', '"weight": 1.45e1') - assert from_json(payload).weight == 14.5 - + header = DppHeader( + digital_product_passport_id=f"urn:example:passport:{variant.value}", + unique_product_identifier="urn:example:product:surface", + granularity="model", + dpp_schema_version="1.0", + dpp_status="active", + last_updated=datetime(2026, 1, 1, tzinfo=UTC), + economic_operator_id="urn:example:operator:surface", + content_specification_ids=( + "DPP4FUN_COMMON_V1", + DPP4FUN_PROFILE.variants[variant.value], + ), + ) + aspect = aspect_type.create(surface=surface_text) + root_extension = SingleValuedDataElement( + element_id="vendorExtension", object_type="SingleValuedDataElement", value="keep" + ) + source = Dpp4Fun.create(header, aspect, extensions=(root_extension,)) + codec = Dpp4FunJsonCodec() -def test_dec_003_non_finite_output_is_causal_mapping_failure( - valid_dpp4fun: Dpp4Fun, -) -> None: - invalid_characteristics = valid_dpp4fun.characteristics.model_copy( - update={"weight": float("inf")} + expanded = codec.from_expanded_json(codec.to_expanded_json(source)) + compressed = codec.from_compressed_json(codec.to_compressed_json(source)) + for value in (expanded, compressed): + canonical = value.passport.data_elements[0].elements[0] + assert isinstance(canonical, SingleValuedDataElement) + assert canonical.value == surface_text + assert isinstance(canonical.value, str) + assert canonical.value_data_type == "xsd:string" + assert value.passport.data_elements[1] == root_extension + typed_root = value.passport.data_elements[0] + assert isinstance(typed_root, DataElementCollection) + typed = aspect_type.from_element(typed_root) + typed_surface = typed.field("surface") + assert isinstance(typed_surface, SingleValuedDataElement) + assert typed_surface.value == surface_text + assert not any( + issue.path.endswith("surface") and issue.rule == "DPP4FUN_BINDING_DATATYPE_MISMATCH" + for issue in Dpp4FunValidationService().validate(value).issues + ) + full_wire = json.loads(codec.to_expanded_json(value)) + full_surface = _find_full_value(full_wire, "surface") + assert full_surface == surface_text + assert _find_full_datatype(full_wire, "surface") == "xsd:string" + compressed_wire = json.loads(codec.to_compressed_json(value)) + assert compressed_wire["characteristics"]["surface"] == surface_text + assert codec.from_expanded_json(codec.to_expanded_json(expanded)).passport == expanded.passport + assert ( + codec.from_compressed_json(codec.to_compressed_json(compressed)).passport + == compressed.passport ) - invalid = valid_dpp4fun.model_copy(update={"characteristics": invalid_characteristics}) + assert source.passport.data_elements[0].elements[0].value == surface_text + + +def _find_full_value(node: object, element_id: str) -> object: + if isinstance(node, dict): + if node.get("elementId") == element_id: + return node.get("value") + for value in node.values(): + found = _find_full_value(value, element_id) + if found is not None: + return found + if isinstance(node, list): + for value in node: + found = _find_full_value(value, element_id) + if found is not None: + return found + return None + + +def _find_full_datatype(node: object, element_id: str) -> object: + if isinstance(node, dict): + if node.get("elementId") == element_id: + return node.get("valueDataType") + for value in node.values(): + found = _find_full_datatype(value, element_id) + if found is not None: + return found + if isinstance(node, list): + for value in node: + found = _find_full_datatype(value, element_id) + if found is not None: + return found + return None + + +def test_python_to_json_names_are_aliases_for_canonical_full_codec() -> None: + codec = Dpp4FunJsonCodec() + value = _value() + assert codec.to_json(value) == codec.to_expanded_json(value) + assert codec.from_json(codec.to_json(value)).passport == value.passport - with pytest.raises(DppMappingError) as exc: - to_json(invalid) - assert isinstance(exc.value.__cause__, ValueError) +@pytest.mark.parametrize("json_text", ["{not-json", "null", "[]"]) +def test_malformed_or_non_object_full_payload_is_mapping_error(json_text: str) -> None: + with pytest.raises(DppJsonException): + Dpp4FunJsonCodec().from_json(json_text) From 60fdd8499118aca262b7d7ef4483cdf0987d7ff5 Mon Sep 17 00:00:00 2001 From: enn35999 Date: Fri, 30 Oct 2026 17:56:27 +0100 Subject: [PATCH 2/2] "Prepare v0.5.0 release" --- CHANGELOG.md | 3 ++- pyproject.toml | 23 +++++++++++++---------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e4f50a..9c4ddec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,9 @@ All notable changes to `dpp-sdk` are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.5.0] - 2026-10-30 +## [Unreleased] +## [0.5.0] - 2026-10-30 ### Added - A canonical `DigitalProductPassport` model with a `DppHeader` and typed DataElement variants, diff --git a/pyproject.toml b/pyproject.toml index 463063f..3aed76b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,14 +5,17 @@ build-backend = "hatchling.build" [project] name = "dpp-sdk" dynamic = ["version"] -description = "Python SDK for interacting with Digital Product Passport (DPP) System Infrastructure " +description = "Python SDK for interacting with Digital Product Passport (DPP) System Infrastructure" readme = "README.md" requires-python = ">=3.11" license = "Apache-2.0" license-files = ["LICENSE"] authors = [{ name = "Enno Mueller" }] -keywords = ["dpp", "digital-product-passport", "prEN-18222", "sustainability"] +keywords = ["dpp", "digital-product-passport", "EN-18222", "EN-18223", "sustainability"] classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -40,6 +43,7 @@ release = [ Homepage = "https://github.com/CIR4FUN-EU/python-dpp-sdk" Repository = "https://github.com/CIR4FUN-EU/python-dpp-sdk" Issues = "https://github.com/CIR4FUN-EU/python-dpp-sdk/issues" +Documentation = "https://github.com/CIR4FUN-EU/python-dpp-sdk/tree/main/docs" Changelog = "https://github.com/CIR4FUN-EU/python-dpp-sdk/blob/main/CHANGELOG.md" [tool.hatch.version] @@ -47,7 +51,7 @@ path = "src/dpp_sdk/__init__.py" [tool.hatch.build.targets.wheel] packages = ["src/dpp_sdk"] -exclude = ["**/AGENTS.md", "**/.codex/**"] +exclude = ["**/AGENTS.md", "**/.codex/**", "**/README.md"] [tool.hatch.build.targets.sdist] include = [ @@ -55,7 +59,12 @@ include = [ "tests", "README.md", "CHANGELOG.md", + "CONTRIBUTING.md", + "docs/*.md", + "docs/architecture/*.mmd", + "docs/architecture/*.svg", "LICENSE", + "examples/consumer_lifecycle.py", ] exclude = [ "examples/mock-services-demo", @@ -87,16 +96,10 @@ target-version = "py311" [tool.ruff.lint] select = ["E", "F", "I", "UP", "B", "SIM", "N"] -# The models and wrapper DTOs deliberately use camelCase field/accessor names to -# match the JSON wire keys of the Java SDK 1:1 — N802/N815 are intentional there. -[tool.ruff.lint.per-file-ignores] -"src/dpp_sdk/core/model.py" = ["N802", "N815"] -"src/dpp_sdk/dpp4fun/model.py" = ["N802", "N815"] -"src/dpp_sdk/clients/payloads.py" = ["N815"] - [tool.mypy] python_version = "3.11" strict = true +plugins = ["pydantic.mypy"] packages = ["dpp_sdk"] mypy_path = "src"