diff --git a/docs/cfn-schema-specification.md b/docs/cfn-schema-specification.md index 57f550d3d9..5d991f7fbe 100644 --- a/docs/cfn-schema-specification.md +++ b/docs/cfn-schema-specification.md @@ -83,9 +83,9 @@ _properties_ provides the key names and a value that represents the schema to va _required_ defines a list of required properties. [JSON Schema docs](https://json-schema.org/understanding-json-schema/reference/object#required) -##### requiredXor +##### requiredOr -_requiredXor_ is used to define when only one property from a set properties is required. +_requiredOr_ is used to define when at least one property from a set properties is required. On the following defined object @@ -104,7 +104,7 @@ The cfn-lint schema ```json { - "requiredXor": ["a", "b", "c"] + "requiredOr": ["a", "b", "c"] } ``` @@ -112,7 +112,7 @@ is equivalent to the JSON schema ```json { - "oneOf": [ + "anyOf": [ { "required": ["a"] }, @@ -126,9 +126,9 @@ is equivalent to the JSON schema } ``` -##### propertiesNand +##### requiredXor -_propertiesNand_ is used to define when none or only one property from a set properties can be defined. +_requiredXor_ is used to define when only one property from a set properties is required. On the following defined object @@ -147,7 +147,7 @@ The cfn-lint schema ```json { - "propertiesNand": ["a", "b", "c"] + "requiredXor": ["a", "b", "c"] } ``` @@ -164,13 +164,6 @@ is equivalent to the JSON schema }, { "required": ["c"] - }, - { - "properties": { - "a": false, - "b": false, - "c": false - } } ] } diff --git a/scripts/update_schemas_manually.py b/scripts/update_schemas_manually.py index 2a95f29bac..daa5b49ce3 100755 --- a/scripts/update_schemas_manually.py +++ b/scripts/update_schemas_manually.py @@ -965,7 +965,7 @@ resource_type="AWS::Events::Rule", patches=[ Patch( - values={"requiredXor": ["EventPattern", "ScheduleExpression"]}, + values={"requiredOr": ["EventPattern", "ScheduleExpression"]}, path="/", ), Patch( diff --git a/src/cfnlint/data/schemas/patches/extensions/all/aws_events_rule/manual.json b/src/cfnlint/data/schemas/patches/extensions/all/aws_events_rule/manual.json index 8f262d3f79..ac3937a76f 100644 --- a/src/cfnlint/data/schemas/patches/extensions/all/aws_events_rule/manual.json +++ b/src/cfnlint/data/schemas/patches/extensions/all/aws_events_rule/manual.json @@ -1,7 +1,7 @@ [ { "op": "add", - "path": "/requiredXor", + "path": "/requiredOr", "value": [ "EventPattern", "ScheduleExpression" diff --git a/src/cfnlint/data/schemas/providers/us_east_1/aws-events-rule.json b/src/cfnlint/data/schemas/providers/us_east_1/aws-events-rule.json index 1cfff71df2..20da1af917 100644 --- a/src/cfnlint/data/schemas/providers/us_east_1/aws-events-rule.json +++ b/src/cfnlint/data/schemas/providers/us_east_1/aws-events-rule.json @@ -535,7 +535,7 @@ "readOnlyProperties": [ "/properties/Arn" ], - "requiredXor": [ + "requiredOr": [ "EventPattern", "ScheduleExpression" ], diff --git a/src/cfnlint/data/schemas/providers/us_gov_east_1/aws-events-rule.json b/src/cfnlint/data/schemas/providers/us_gov_east_1/aws-events-rule.json index c3561eff24..85e51ef330 100644 --- a/src/cfnlint/data/schemas/providers/us_gov_east_1/aws-events-rule.json +++ b/src/cfnlint/data/schemas/providers/us_gov_east_1/aws-events-rule.json @@ -496,7 +496,7 @@ "/properties/Id", "/properties/Arn" ], - "requiredXor": [ + "requiredOr": [ "EventPattern", "ScheduleExpression" ], diff --git a/src/cfnlint/data/schemas/providers/us_gov_west_1/aws-events-rule.json b/src/cfnlint/data/schemas/providers/us_gov_west_1/aws-events-rule.json index c3561eff24..85e51ef330 100644 --- a/src/cfnlint/data/schemas/providers/us_gov_west_1/aws-events-rule.json +++ b/src/cfnlint/data/schemas/providers/us_gov_west_1/aws-events-rule.json @@ -496,7 +496,7 @@ "/properties/Id", "/properties/Arn" ], - "requiredXor": [ + "requiredOr": [ "EventPattern", "ScheduleExpression" ], diff --git a/src/cfnlint/jsonschema/_filter.py b/src/cfnlint/jsonschema/_filter.py index 32855a8b56..5fa077897e 100644 --- a/src/cfnlint/jsonschema/_filter.py +++ b/src/cfnlint/jsonschema/_filter.py @@ -45,7 +45,7 @@ class FunctionFilter: "minItems", "minProperties", "required", - "requiredAtLeastOne", + "requiredOr", "requiredXor", "then", "uniqueItems", diff --git a/src/cfnlint/jsonschema/_keywords.py b/src/cfnlint/jsonschema/_keywords.py index c33dde4935..635057165b 100644 --- a/src/cfnlint/jsonschema/_keywords.py +++ b/src/cfnlint/jsonschema/_keywords.py @@ -541,6 +541,17 @@ def required( yield ValidationError(f"{property!r} is a required property") +def requiredOr( + validator: Validator, required: Any, instance: Any, schema: dict[str, Any] +) -> ValidationResult: + if not validator.is_type(instance, "object"): + return + matches = set(required).intersection(instance.keys()) + if not matches: + yield ValidationError(f"One of {required!r} is a required property") + return + + def requiredXor( validator: Validator, required: Any, instance: Any, schema: dict[str, Any] ) -> ValidationResult: diff --git a/src/cfnlint/jsonschema/validators.py b/src/cfnlint/jsonschema/validators.py index ca6e16eefc..3e42afbd94 100644 --- a/src/cfnlint/jsonschema/validators.py +++ b/src/cfnlint/jsonschema/validators.py @@ -396,6 +396,7 @@ def extend( "properties": _keywords.properties, "propertyNames": _keywords.propertyNames, "required": _keywords.required, + "requiredOr": _keywords.requiredOr, "requiredXor": _keywords.requiredXor, "type": _keywords.type, "uniqueItems": _keywords.uniqueItems, diff --git a/src/cfnlint/rules/resources/properties/Properties.py b/src/cfnlint/rules/resources/properties/Properties.py index 94c91bcd09..90029b7758 100644 --- a/src/cfnlint/rules/resources/properties/Properties.py +++ b/src/cfnlint/rules/resources/properties/Properties.py @@ -37,6 +37,7 @@ def __init__(self): "dependentExcluded": "E3020", "dependentRequired": "E3021", "required": "E3003", + "requiredOr": "E3058", "requiredXor": "E3014", "enum": "E3030", "type": "E3012", diff --git a/src/cfnlint/rules/resources/properties/RequiredOr.py b/src/cfnlint/rules/resources/properties/RequiredOr.py new file mode 100644 index 0000000000..67724d4e83 --- /dev/null +++ b/src/cfnlint/rules/resources/properties/RequiredOr.py @@ -0,0 +1,14 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" + +from cfnlint.rules import CloudFormationLintRule + + +class RequiredOr(CloudFormationLintRule): + id = "E3058" + shortdesc = "Validate at least one of the properties are required" + description = "Make sure at least one of the resource properties are included" + source_url = "https://github.com/aws-cloudformation/cfn-lint/blob/main/docs/cfn-schema-specification.md#requiredor" + tags = ["resources"] diff --git a/test/unit/module/jsonschema/test_validator.py b/test/unit/module/jsonschema/test_validator.py index f981aadd58..863195472d 100644 --- a/test/unit/module/jsonschema/test_validator.py +++ b/test/unit/module/jsonschema/test_validator.py @@ -947,6 +947,28 @@ def test_validator(name, schema, instance, expected, validator): ) ], ), + ( + "valid requiredOr", + {"requiredOr": ["foo", "bar"]}, + {"foo": {}}, + [], + ), + ( + "valid requiredOr with wrong type", + {"requiredOr": ["foo", "bar"]}, + [], + [], + ), + ( + "invalid requiredOr with empty object", + {"requiredOr": ["foo", "bar"]}, + {}, + [ + ValidationError( + "One of ['foo', 'bar'] is a required property", + ) + ], + ), ( "valid requiredXor", {"requiredXor": ["foo", "bar"]},