diff --git a/sphinx_needs/schema/core.py b/sphinx_needs/schema/core.py index c3d679cad..994c191c3 100644 --- a/sphinx_needs/schema/core.py +++ b/sphinx_needs/schema/core.py @@ -439,35 +439,47 @@ def reduce_need( field_properties: Mapping[str, NeedFieldProperties], schema_properties: set[str], ) -> dict[str, Any]: - """ - Reduce a need to its relevant fields for validation in a specific schema context. + """Reduce a need to the subset relevant for schema validation. + + The returned mapping is used as the validation instance (especially for + ``unevaluatedProperties``) to focus on user-controlled fields. + + Rules: + + - ``None`` values are always dropped (schema validation cannot handle null types). + - Extra fields are included based on their configured default (independent of + ``schema_properties``): - The reduction is required to separated actively set fields from defaults. - Also internal fields shall be removed, if they are not actively used in the schema. - This is required to make unevaluatedProperties work as expected which disallows - additional fields. + - If the default is ``None``, any non-``None`` value is kept. + - If the default is ``""`` (SN5-style empty), the value is kept only if it is not ``""``. + - For any other default value, the field is kept as-is. - Needs can be reduced in multiple contexts as the need can be primary target of validation - or it can be a link target which might mean only a single field shall be checked for a - specific value. + - Link fields are included only if they are non-empty (independent of ``schema_properties``). + - Core fields are included if they are covered by ``schema_properties``. + Additionally, ``status`` is kept if it is non-``None`` and ``tags`` is kept if it is non-empty. - Fields are kept - - if they are extra fields and differ from their default value - - if they are links and the list is not empty - - if they are part of the user provided schema + SN5 vs SN6 empty-vs-missing semantics for extra options: - :param need: The need to reduce. - :param json_schema: The user provided and merged JSON merge. + - Without a field schema (SN5-style), unset and present-but-empty (``:opt:``) both become + ``""``, so "unset" and "set-but-empty" cannot be distinguished. + - With a field schema (SN6), unset nullable options default to ``None``, while ``:opt:`` + results in ``""``. This allows distinguishing "unset" from "set-but-empty". + + In this reduction, that distinction matters for extra fields: ``""`` is dropped if the + default is also ``""`` (SN5-style), while ``""`` is kept if the default is ``None`` + (SN6-style), so set-but-empty can be treated as actively set. + + For link fields, missing vs empty cannot be distinguished (both result in ``[]``). + + :param need: Need to reduce. + :param field_properties: Field defaults/types used to detect default values. + :param schema_properties: Set of field names covered by the current schema. + :return: Reduced mapping used as schema validation instance. """ reduced_need: dict[str, Any] = {} for field, value in need.iter_extra_items(): - if value is None: - # value is not provided - continue - schema_field = field_properties[field] - if not ("default" in schema_field and value == schema_field["default"]): - # keep explicitly set extra options + if value is not None: reduced_need[field] = value for field, value in need.iter_links_items(): @@ -477,14 +489,20 @@ def reduce_need( for field, value in need.iter_core_items(): if value is None: - # value is not provided continue - schema_field = field_properties[field] - if field in schema_properties and not ( - "default" in schema_field and value == schema_field["default"] - ): - # keep core field, it has no default or differs from the default and - # is part of the user provided schema + + if field in schema_properties: + # actively included by schema - include to validate constraints such as docname + # useful for both 'select' and 'validate' schemas + # TODO(Marco): think about how to remove this dependency on schema_properties; + # it's a performance issue + reduced_need[field] = value + continue + + if field == "status": # default is None + reduced_need[field] = value + + if field == "tags" and len(value) > 0: # default is [] reduced_need[field] = value return reduced_need diff --git a/tests/schema/__snapshots__/test_schema.ambr b/tests/schema/__snapshots__/test_schema.ambr index bf35f61c5..dd2d3283d 100644 --- a/tests/schema/__snapshots__/test_schema.ambr +++ b/tests/schema/__snapshots__/test_schema.ambr @@ -4758,7 +4758,173 @@ }), }) # --- -# name: test_schemas[schema/fixtures/unevaluated-unevaluated_allof_error] +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_core_docname_in_schema_and_invalid] + ''' + ERROR: Need 'IMPL_1' has schema violations: + Severity: violation + Field: docname + Need path: IMPL_1 + Schema path: [0] > local > properties > docname > enum + Schema message: "index" is not one of "some_other_doc" [sn_schema_violation.local_fail] + + ''' +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_core_docname_in_schema_and_invalid].1 + dict({ + 'validated_needs_count': 1, + 'validation_warnings': dict({ + 'IMPL_1': list([ + dict({ + 'children': list([ + ]), + 'details': dict({ + 'field': 'docname', + 'need_path': 'IMPL_1', + 'schema_path': '[0] > local > properties > docname > enum', + 'severity': 'violation', + 'validation_msg': '"index" is not one of "some_other_doc"', + }), + 'log_lvl': 'error', + 'subtype': 'local_fail', + 'type': 'sn_schema_violation', + }), + ]), + }), + }) +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_core_status_in_schema_and_invalid] + ''' + ERROR: Need 'IMPL_1' has schema violations: + Severity: violation + Field: status + Need path: IMPL_1 + Schema path: [0] > local > properties > status > enum + Schema message: "OPEN" is not one of "CLOSED" [sn_schema_violation.local_fail] + + ''' +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_core_status_in_schema_and_invalid].1 + dict({ + 'validated_needs_count': 1, + 'validation_warnings': dict({ + 'IMPL_1': list([ + dict({ + 'children': list([ + ]), + 'details': dict({ + 'field': 'status', + 'need_path': 'IMPL_1', + 'schema_path': '[0] > local > properties > status > enum', + 'severity': 'violation', + 'validation_msg': '"OPEN" is not one of "CLOSED"', + }), + 'log_lvl': 'error', + 'subtype': 'local_fail', + 'type': 'sn_schema_violation', + }), + ]), + }), + }) +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_core_status_set_not_in_schema] + ''' + ERROR: Need 'IMPL_1' has schema violations: + Severity: violation + Need path: IMPL_1 + Schema path: [0] > local > unevaluatedProperties + Schema message: Unevaluated properties are not allowed ('status' was unexpected) [sn_schema_violation.local_fail] + + ''' +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_core_status_set_not_in_schema].1 + dict({ + 'validated_needs_count': 1, + 'validation_warnings': dict({ + 'IMPL_1': list([ + dict({ + 'children': list([ + ]), + 'details': dict({ + 'need_path': 'IMPL_1', + 'schema_path': '[0] > local > unevaluatedProperties', + 'severity': 'violation', + 'validation_msg': "Unevaluated properties are not allowed ('status' was unexpected)", + }), + 'log_lvl': 'error', + 'subtype': 'local_fail', + 'type': 'sn_schema_violation', + }), + ]), + }), + }) +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_core_tags_in_schema_and_invalid] + ''' + ERROR: Need 'IMPL_1' has schema violations: + Severity: violation + Field: tags.0 + Need path: IMPL_1 + Schema path: [0] > local > properties > tags > items > enum + Schema message: "tag_a" is not one of "tag_b" [sn_schema_violation.local_fail] + + ''' +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_core_tags_in_schema_and_invalid].1 + dict({ + 'validated_needs_count': 1, + 'validation_warnings': dict({ + 'IMPL_1': list([ + dict({ + 'children': list([ + ]), + 'details': dict({ + 'field': 'tags.0', + 'need_path': 'IMPL_1', + 'schema_path': '[0] > local > properties > tags > items > enum', + 'severity': 'violation', + 'validation_msg': '"tag_a" is not one of "tag_b"', + }), + 'log_lvl': 'error', + 'subtype': 'local_fail', + 'type': 'sn_schema_violation', + }), + ]), + }), + }) +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_core_tags_set_not_in_schema] + ''' + ERROR: Need 'IMPL_1' has schema violations: + Severity: violation + Need path: IMPL_1 + Schema path: [0] > local > unevaluatedProperties + Schema message: Unevaluated properties are not allowed ('tags' was unexpected) [sn_schema_violation.local_fail] + + ''' +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_core_tags_set_not_in_schema].1 + dict({ + 'validated_needs_count': 1, + 'validation_warnings': dict({ + 'IMPL_1': list([ + dict({ + 'children': list([ + ]), + 'details': dict({ + 'need_path': 'IMPL_1', + 'schema_path': '[0] > local > unevaluatedProperties', + 'severity': 'violation', + 'validation_msg': "Unevaluated properties are not allowed ('tags' was unexpected)", + }), + 'log_lvl': 'error', + 'subtype': 'local_fail', + 'type': 'sn_schema_violation', + }), + ]), + }), + }) +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_error_allof] ''' ERROR: Need 'IMPL_1' has schema violations: Severity: violation @@ -4768,7 +4934,7 @@ ''' # --- -# name: test_schemas[schema/fixtures/unevaluated-unevaluated_allof_error].1 +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_error_allof].1 dict({ 'validated_needs_count': 1, 'validation_warnings': dict({ @@ -4790,7 +4956,103 @@ }), }) # --- -# name: test_schemas[schema/fixtures/unevaluated-unevaluated_error] +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_error_core_status_with_default] + ''' + ERROR: Need 'IMPL_1' has schema violations: + Severity: violation + Need path: IMPL_1 + Schema path: [0] > local > unevaluatedProperties + Schema message: Unevaluated properties are not allowed ('status' was unexpected) [sn_schema_violation.local_fail] + + ''' +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_error_core_status_with_default].1 + dict({ + 'validated_needs_count': 1, + 'validation_warnings': dict({ + 'IMPL_1': list([ + dict({ + 'children': list([ + ]), + 'details': dict({ + 'need_path': 'IMPL_1', + 'schema_path': '[0] > local > unevaluatedProperties', + 'severity': 'violation', + 'validation_msg': "Unevaluated properties are not allowed ('status' was unexpected)", + }), + 'log_lvl': 'error', + 'subtype': 'local_fail', + 'type': 'sn_schema_violation', + }), + ]), + }), + }) +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_error_core_tags_with_default] + ''' + ERROR: Need 'IMPL_1' has schema violations: + Severity: violation + Need path: IMPL_1 + Schema path: [0] > local > unevaluatedProperties + Schema message: Unevaluated properties are not allowed ('tags' was unexpected) [sn_schema_violation.local_fail] + + ''' +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_error_core_tags_with_default].1 + dict({ + 'validated_needs_count': 1, + 'validation_warnings': dict({ + 'IMPL_1': list([ + dict({ + 'children': list([ + ]), + 'details': dict({ + 'need_path': 'IMPL_1', + 'schema_path': '[0] > local > unevaluatedProperties', + 'severity': 'violation', + 'validation_msg': "Unevaluated properties are not allowed ('tags' was unexpected)", + }), + 'log_lvl': 'error', + 'subtype': 'local_fail', + 'type': 'sn_schema_violation', + }), + ]), + }), + }) +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_error_extra_with_default] + ''' + ERROR: Need 'IMPL_1' has schema violations: + Severity: violation + Need path: IMPL_1 + Schema path: [0] > local > unevaluatedProperties + Schema message: Unevaluated properties are not allowed ('comment' was unexpected) [sn_schema_violation.local_fail] + + ''' +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_error_extra_with_default].1 + dict({ + 'validated_needs_count': 1, + 'validation_warnings': dict({ + 'IMPL_1': list([ + dict({ + 'children': list([ + ]), + 'details': dict({ + 'need_path': 'IMPL_1', + 'schema_path': '[0] > local > unevaluatedProperties', + 'severity': 'violation', + 'validation_msg': "Unevaluated properties are not allowed ('comment' was unexpected)", + }), + 'log_lvl': 'error', + 'subtype': 'local_fail', + 'type': 'sn_schema_violation', + }), + ]), + }), + }) +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_error_extra_with_schema_given_in_rst_no_constraint] ''' ERROR: Need 'IMPL_1' has schema violations: Severity: violation @@ -4800,7 +5062,7 @@ ''' # --- -# name: test_schemas[schema/fixtures/unevaluated-unevaluated_error].1 +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_error_extra_with_schema_given_in_rst_no_constraint].1 dict({ 'validated_needs_count': 1, 'validation_warnings': dict({ @@ -4822,3 +5084,107 @@ }), }) # --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_extra_missing_schema_const_missing_in_rst] + '' +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_extra_missing_schema_const_missing_in_rst].1 + dict({ + 'validated_needs_count': 1, + 'validation_warnings': dict({ + }), + }) +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_extra_no_schema_given_in_rst] + '' +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_extra_no_schema_given_in_rst].1 + dict({ + 'validated_needs_count': 1, + 'validation_warnings': dict({ + }), + }) +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_extra_no_schema_missing_in_rst] + '' +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_extra_no_schema_missing_in_rst].1 + dict({ + 'validated_needs_count': 1, + 'validation_warnings': dict({ + }), + }) +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_extra_with_schema_const_empty_in_rst] + ''' + ERROR: Need 'IMPL_1' has schema violations: + Severity: violation + Field: comment + Need path: IMPL_1 + Schema path: [0] > local > properties > comment > const + Schema message: "expected" was expected [sn_schema_violation.local_fail] + + ''' +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_extra_with_schema_const_empty_in_rst].1 + dict({ + 'validated_needs_count': 1, + 'validation_warnings': dict({ + 'IMPL_1': list([ + dict({ + 'children': list([ + ]), + 'details': dict({ + 'field': 'comment', + 'need_path': 'IMPL_1', + 'schema_path': '[0] > local > properties > comment > const', + 'severity': 'violation', + 'validation_msg': '"expected" was expected', + }), + 'log_lvl': 'error', + 'subtype': 'local_fail', + 'type': 'sn_schema_violation', + }), + ]), + }), + }) +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_extra_with_schema_const_missing_in_rst] + '' +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_extra_with_schema_const_missing_in_rst].1 + dict({ + 'validated_needs_count': 1, + 'validation_warnings': dict({ + }), + }) +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_extra_with_schema_missing_in_rst] + '' +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_extra_with_schema_missing_in_rst].1 + dict({ + 'validated_needs_count': 1, + 'validation_warnings': dict({ + }), + }) +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_link_empty_value] + '' +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_link_empty_value].1 + dict({ + 'validated_needs_count': 1, + 'validation_warnings': dict({ + }), + }) +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_link_missing] + '' +# --- +# name: test_schemas[schema/fixtures/unevaluated-unevaluated_link_missing].1 + dict({ + 'validated_needs_count': 1, + 'validation_warnings': dict({ + }), + }) +# --- diff --git a/tests/schema/fixtures/unevaluated.yml b/tests/schema/fixtures/unevaluated.yml index 50ecc965e..cca81e360 100644 --- a/tests/schema/fixtures/unevaluated.yml +++ b/tests/schema/fixtures/unevaluated.yml @@ -1,4 +1,14 @@ -unevaluated_error: +# 1. unevaluatedProperties: Anything not listed in 'properties' or 'required' fields +# of schemas has to be null. +# 2. empty string is a value actively set +# 3. None is used for missing values of fields +# 4. [] is used for missing values of link fields and the `tags` field (empty fields arrays use +# None, this should be streamlined) +# TODO(Marco): streamline this behavior + +unevaluated_extra_no_schema_missing_in_rst: + # should not complain, the comment field is unset + # as there is no schema, the default is empty string, so unevaluatedProperties does not see it conf: | extensions = ["sphinx_needs"] needs_from_toml = "ubproject.toml" @@ -10,7 +20,6 @@ unevaluated_error: .. impl:: title :id: IMPL_1 :asil: QM - :comment: This is a comment schemas: schemas: - validate: @@ -18,7 +27,179 @@ unevaluated_error: properties: {"asil": {}} unevaluatedProperties: false -unevaluated_allof_error: +unevaluated_extra_no_schema_given_in_rst: + # should not complain, the comment field is set, but as there is no schema; + # the default is also an empty string, so the field is removed from the need and + # unevaluatedProperties does not see it + + # in a nullable world, comment would be an empty string as it is given; + # so should lean to an error + conf: | + extensions = ["sphinx_needs"] + needs_from_toml = "ubproject.toml" + needs_schema_definitions_from_json = "schemas.json" + ubproject: | + [needs.fields.asil] + [needs.fields.comment] + rst: | + .. impl:: title + :id: IMPL_1 + :asil: QM + :comment: + schemas: + schemas: + - validate: + local: + properties: {"asil": {}} + unevaluatedProperties: false + +unevaluated_extra_with_schema_missing_in_rst: + # should not complain, the comment field is missing in the need; + # as it has a schema, the default is None, so the field is removed from validation + # and unevaluatedProperties does not see it + conf: | + extensions = ["sphinx_needs"] + needs_from_toml = "ubproject.toml" + needs_schema_definitions_from_json = "schemas.json" + ubproject: | + [needs.fields.asil] + [needs.fields.comment] + schema.type = "string" + rst: | + .. impl:: title + :id: IMPL_1 + :asil: QM + schemas: + schemas: + - validate: + local: + properties: {"asil": {}} + unevaluatedProperties: false + +unevaluated_extra_with_schema_const_missing_in_rst: + # should not complain, because the comment field has a schema; not providing a value + # leads to a None value which cannot be handled by schemas.json and gets removed before + # validation; the const validation is not triggered on a non-existing field + conf: | + extensions = ["sphinx_needs"] + needs_from_toml = "ubproject.toml" + needs_schema_definitions_from_json = "schemas.json" + ubproject: | + [needs.fields.asil] + [needs.fields.comment] + schema.type = "string" + rst: | + .. impl:: title + :id: IMPL_1 + :asil: QM + schemas: + schemas: + - validate: + local: + properties: + asil: {} + comment: {"const": "expected"} + unevaluatedProperties: false + +unevaluated_extra_with_schema_const_empty_in_rst: + # should complain, because the comment field has a schema; the field is given empty + # which is represented as empty string, which does not match the constraint + conf: | + extensions = ["sphinx_needs"] + needs_from_toml = "ubproject.toml" + needs_schema_definitions_from_json = "schemas.json" + ubproject: | + [needs.fields.asil] + [needs.fields.comment] + schema.type = "string" + rst: | + .. impl:: title + :id: IMPL_1 + :asil: QM + :comment: + schemas: + schemas: + - validate: + local: + properties: + asil: {} + comment: {"const": "expected"} + unevaluatedProperties: false + +unevaluated_extra_missing_schema_const_missing_in_rst: + # should not complain, the field has no schema constraint, so the default is an empty string; + # the value does not differ from this, which leads to the removal of the field for validation; + # the behavior is identical to the behavior with field schema + conf: | + extensions = ["sphinx_needs"] + needs_from_toml = "ubproject.toml" + needs_schema_definitions_from_json = "schemas.json" + ubproject: | + [needs.fields.asil] + [needs.fields.comment] + rst: | + .. impl:: title + :id: IMPL_1 + :asil: QM + schemas: + schemas: + - validate: + local: + properties: + asil: {} + comment: {"const": "expected"} + unevaluatedProperties: false + +unevaluated_error_extra_with_schema_given_in_rst_no_constraint: + # should complain, the comment field is set, and it has a schema, so SN detects it is actively + # given as '', and it will appear in the unevaluatedProperties evaluation as forbidden + # additional field + conf: | + extensions = ["sphinx_needs"] + needs_from_toml = "ubproject.toml" + needs_schema_definitions_from_json = "schemas.json" + ubproject: | + [needs.fields.asil] + [needs.fields.comment] + schema.type = "string" + rst: | + .. impl:: title + :id: IMPL_1 + :asil: QM + :comment: + schemas: + schemas: + - validate: + local: + properties: {"asil": {}} + unevaluatedProperties: false + +unevaluated_error_extra_with_default: + # should complain, the comment field is set via default, and it will appear in the + # unevaluatedProperties evaluation as forbidden additional field + conf: | + extensions = ["sphinx_needs"] + needs_from_toml = "ubproject.toml" + needs_schema_definitions_from_json = "schemas.json" + ubproject: | + [needs.fields.asil] + [needs.fields.comment] + schema.type = "string" + default = "default" + rst: | + .. impl:: title + :id: IMPL_1 + :asil: QM + schemas: + schemas: + - validate: + local: + properties: {"asil": {}} + unevaluatedProperties: false + +unevaluated_error_allof: + # check whether unevaluatedProperties implementation applies to combined schemas (allOf) + # should complain about "approved" given as additional field, but not for comment or asil conf: | extensions = ["sphinx_needs"] needs_from_toml = "ubproject.toml" @@ -42,3 +223,206 @@ unevaluated_allof_error: unevaluatedProperties: false allOf: - properties: {"comment": {}} + +unevaluated_link_empty_value: + # should not complain, the link field is set, but empty, so it is removed from validation + # and unevaluatedProperties does not see it + conf: | + extensions = ["sphinx_needs"] + needs_from_toml = "ubproject.toml" + needs_schema_definitions_from_json = "schemas.json" + ubproject: | + [needs.fields.asil] + [[needs.extra_links]] + option = "relates" + outgoing = "relates" + incoming = "related by" + rst: | + .. impl:: title + :id: IMPL_1 + :asil: QM + :relates: + schemas: + schemas: + - validate: + local: + properties: {"asil": {}} + unevaluatedProperties: false + +unevaluated_link_missing: + # should not complain, the link field is missing in the need; + # unset link fields are given as empty list [] in the model, so cannot distinguish from default + # which is also []; as it is not set, it is removed from validation and unevaluatedProperties + # does not see it + conf: | + extensions = ["sphinx_needs"] + needs_from_toml = "ubproject.toml" + needs_schema_definitions_from_json = "schemas.json" + ubproject: | + [needs.fields.asil] + [[needs.extra_links]] + option = "relates" + outgoing = "relates" + incoming = "related by" + rst: | + .. impl:: title + :id: IMPL_1 + :asil: QM + schemas: + schemas: + - validate: + local: + properties: {"asil": {}} + unevaluatedProperties: false + +unevaluated_core_status_set_not_in_schema: + # should complain, because the core field status is treated like an extra field, + # so it is kept when explicitly set and triggers unevaluatedProperties. + conf: | + extensions = ["sphinx_needs"] + needs_from_toml = "ubproject.toml" + needs_schema_definitions_from_json = "schemas.json" + ubproject: | + [needs.fields.asil] + rst: | + .. impl:: title + :id: IMPL_1 + :asil: QM + :status: OPEN + schemas: + schemas: + - validate: + local: + properties: {"asil": {}} + unevaluatedProperties: false + +unevaluated_core_status_in_schema_and_invalid: + # should complain about status, because it is part of the user schema and actively set, + # therefore it must be kept in the reduced need and validated + conf: | + extensions = ["sphinx_needs"] + needs_from_toml = "ubproject.toml" + needs_schema_definitions_from_json = "schemas.json" + ubproject: | + [needs.fields.asil] + rst: | + .. impl:: title + :id: IMPL_1 + :asil: QM + :status: OPEN + schemas: + schemas: + - validate: + local: + properties: {"asil": {}, "status": {"enum": ["CLOSED"]}} + unevaluatedProperties: false + +unevaluated_error_core_status_with_default: + # should complain, the status field has a default, and it will appear in the + # unevaluatedProperties evaluation as forbidden additional field + conf: | + extensions = ["sphinx_needs"] + needs_from_toml = "ubproject.toml" + needs_schema_definitions_from_json = "schemas.json" + ubproject: | + [needs.fields.asil] + [needs.fields.status] + default = "default" + rst: | + .. impl:: title + :id: IMPL_1 + :asil: QM + schemas: + schemas: + - validate: + local: + properties: {"asil": {}} + unevaluatedProperties: false + +unevaluated_core_tags_set_not_in_schema: + # should complain, because the core field tags is treated like an extra field, + # so it is kept when explicitly set and triggers unevaluatedProperties. + conf: | + extensions = ["sphinx_needs"] + needs_from_toml = "ubproject.toml" + needs_schema_definitions_from_json = "schemas.json" + ubproject: | + [needs.fields.asil] + rst: | + .. impl:: title + :id: IMPL_1 + :asil: QM + :tags: tag_a + schemas: + schemas: + - validate: + local: + properties: {"asil": {}} + unevaluatedProperties: false + +unevaluated_core_tags_in_schema_and_invalid: + # should complain about tags, because it is part of the user schema and actively set, + # therefore it must be kept in the reduced need and validated + conf: | + extensions = ["sphinx_needs"] + needs_from_toml = "ubproject.toml" + needs_schema_definitions_from_json = "schemas.json" + ubproject: | + [needs.fields.asil] + rst: | + .. impl:: title + :id: IMPL_1 + :asil: QM + :tags: tag_a + schemas: + schemas: + - validate: + local: + properties: + asil: {} + tags: + type: array + items: {type: string, enum: ["tag_b"]} + unevaluatedProperties: false + +unevaluated_error_core_tags_with_default: + # should complain, the tags field has a default, and it will appear in the + # unevaluatedProperties evaluation as forbidden additional field + conf: | + extensions = ["sphinx_needs"] + needs_from_toml = "ubproject.toml" + needs_schema_definitions_from_json = "schemas.json" + ubproject: | + [needs.fields.asil] + [needs.fields.tags] + default = ["default"] + rst: | + .. impl:: title + :id: IMPL_1 + :asil: QM + schemas: + schemas: + - validate: + local: + properties: {"asil": {}} + unevaluatedProperties: false + +unevaluated_core_docname_in_schema_and_invalid: + # should complain about docname, because it is part of the user schema, + # therefore it is kept in the reduced need and validated + conf: | + extensions = ["sphinx_needs"] + needs_from_toml = "ubproject.toml" + needs_schema_definitions_from_json = "schemas.json" + ubproject: | + [needs.fields.asil] + rst: | + .. impl:: title + :id: IMPL_1 + :asil: QM + schemas: + schemas: + - validate: + local: + properties: {"asil": {}, "docname": {"enum": ["some_other_doc"]}} + unevaluatedProperties: false