diff --git a/docs/generators/java.md b/docs/generators/java.md index 428bb9ec34f..5091da9c25b 100644 --- a/docs/generators/java.md +++ b/docs/generators/java.md @@ -342,6 +342,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |Nullable|✗|OAS3 |OneOf|✗|OAS3 |Pattern|✓|OAS2,OAS3 +|PatternProperties|✗|OAS3 |Properties|✓|OAS2,OAS3 |PropertyNames|✗|OAS3 |Required|✓|OAS2,OAS3 diff --git a/docs/generators/jaxrs-jersey.md b/docs/generators/jaxrs-jersey.md index f8ebd848027..a9fc112a2df 100644 --- a/docs/generators/jaxrs-jersey.md +++ b/docs/generators/jaxrs-jersey.md @@ -325,6 +325,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |Nullable|✗|OAS3 |OneOf|✗|OAS3 |Pattern|✓|OAS2,OAS3 +|PatternProperties|✗|OAS3 |Properties|✓|OAS2,OAS3 |PropertyNames|✗|OAS3 |Required|✓|OAS2,OAS3 diff --git a/docs/generators/jmeter.md b/docs/generators/jmeter.md index 66408fe5ffd..62be6c00297 100644 --- a/docs/generators/jmeter.md +++ b/docs/generators/jmeter.md @@ -184,6 +184,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |Nullable|✗|OAS3 |OneOf|✗|OAS3 |Pattern|✓|OAS2,OAS3 +|PatternProperties|✗|OAS3 |Properties|✓|OAS2,OAS3 |PropertyNames|✗|OAS3 |Required|✓|OAS2,OAS3 diff --git a/docs/generators/kotlin.md b/docs/generators/kotlin.md index 29cac22fdc2..b29b0de4d41 100644 --- a/docs/generators/kotlin.md +++ b/docs/generators/kotlin.md @@ -294,6 +294,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |Nullable|✗|OAS3 |OneOf|✗|OAS3 |Pattern|✓|OAS2,OAS3 +|PatternProperties|✗|OAS3 |Properties|✓|OAS2,OAS3 |PropertyNames|✗|OAS3 |Required|✓|OAS2,OAS3 diff --git a/docs/generators/python.md b/docs/generators/python.md index 7b3564fe91e..9c62f3710dd 100644 --- a/docs/generators/python.md +++ b/docs/generators/python.md @@ -253,6 +253,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |Nullable|✓|OAS3 |OneOf|✓|OAS3 |Pattern|✓|OAS2,OAS3 +|PatternProperties|✓|OAS3 |Properties|✓|OAS2,OAS3 |PropertyNames|✓|OAS3 |Required|✓|OAS2,OAS3 diff --git a/samples/client/3_0_3_unit_test/python/src/unit_test_api/schemas/validation.py b/samples/client/3_0_3_unit_test/python/src/unit_test_api/schemas/validation.py index cd967028d63..511f1cbb2df 100644 --- a/samples/client/3_0_3_unit_test/python/src/unit_test_api/schemas/validation.py +++ b/samples/client/3_0_3_unit_test/python/src/unit_test_api/schemas/validation.py @@ -1124,6 +1124,36 @@ def validate_property_names( return None +def validate_pattern_properties( + arg: typing.Any, + pattern_properties: typing.Mapping[PatternInfo, typing.Type[SchemaValidator]], + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> typing.Optional[PathToSchemasType]: + if not isinstance(arg, immutabledict): + return None + path_to_schemas: PathToSchemasType = {} + module_namespace = vars(sys.modules[cls.__module__]) + for property_name, property_value in arg.items(): + path_to_item = validation_metadata.path_to_item + (property_name,) + property_validation_metadata = ValidationMetadata( + path_to_item=path_to_item, + configuration=validation_metadata.configuration, + validated_path_to_schemas=validation_metadata.validated_path_to_schemas + ) + for pattern_info, schema in pattern_properties.items(): + flags = pattern_info.flags if pattern_info.flags is not None else 0 + if not re.search(pattern_info.pattern, property_name, flags=flags): + continue + schema = _get_class(schema, module_namespace) + if validation_metadata.validation_ran_earlier(schema): + add_deeper_validated_schemas(validation_metadata, path_to_schemas) + continue + other_path_to_schemas = schema._validate(property_value, validation_metadata=property_validation_metadata) + update(path_to_schemas, other_path_to_schemas) + return path_to_schemas + + validator_type = typing.Callable[[typing.Any, typing.Any, type, ValidationMetadata], typing.Optional[PathToSchemasType]] json_schema_keyword_to_validator: typing.Mapping[str, validator_type] = { 'types': validate_types, @@ -1157,5 +1187,6 @@ def validate_property_names( 'const_value_to_name': validate_const, 'dependent_required': validate_dependent_required, 'dependent_schemas': validate_dependent_schemas, - 'property_names': validate_property_names + 'property_names': validate_property_names, + 'pattern_properties': validate_pattern_properties } \ No newline at end of file diff --git a/samples/client/3_1_0_json_schema/python/.openapi-generator/FILES b/samples/client/3_1_0_json_schema/python/.openapi-generator/FILES index ad591a8d432..81606864953 100644 --- a/samples/client/3_1_0_json_schema/python/.openapi-generator/FILES +++ b/samples/client/3_1_0_json_schema/python/.openapi-generator/FILES @@ -9,12 +9,14 @@ docs/components/schema/any_type_dependent_required.md docs/components/schema/any_type_dependent_schemas.md docs/components/schema/any_type_max_contains_value.md docs/components/schema/any_type_min_contains_value.md +docs/components/schema/any_type_pattern_properties.md docs/components/schema/any_type_property_names.md docs/components/schema/array_contains_value.md docs/components/schema/array_max_contains_value.md docs/components/schema/array_min_contains_value.md docs/components/schema/object_dependent_required.md docs/components/schema/object_dependent_schemas.md +docs/components/schema/object_pattern_properties.md docs/components/schema/object_property_names.md docs/components/schema/string_const_string.md docs/paths/some_path/get.md @@ -43,12 +45,14 @@ src/json_schema_api/components/schema/any_type_dependent_required.py src/json_schema_api/components/schema/any_type_dependent_schemas.py src/json_schema_api/components/schema/any_type_max_contains_value.py src/json_schema_api/components/schema/any_type_min_contains_value.py +src/json_schema_api/components/schema/any_type_pattern_properties.py src/json_schema_api/components/schema/any_type_property_names.py src/json_schema_api/components/schema/array_contains_value.py src/json_schema_api/components/schema/array_max_contains_value.py src/json_schema_api/components/schema/array_min_contains_value.py src/json_schema_api/components/schema/object_dependent_required.py src/json_schema_api/components/schema/object_dependent_schemas.py +src/json_schema_api/components/schema/object_pattern_properties.py src/json_schema_api/components/schema/object_property_names.py src/json_schema_api/components/schema/string_const_string.py src/json_schema_api/components/schemas/__init__.py diff --git a/samples/client/3_1_0_json_schema/python/README.md b/samples/client/3_1_0_json_schema/python/README.md index dca86e71225..5f679d6ee53 100644 --- a/samples/client/3_1_0_json_schema/python/README.md +++ b/samples/client/3_1_0_json_schema/python/README.md @@ -179,12 +179,14 @@ Class | Description [AnyTypeDependentSchemas](docs/components/schema/any_type_dependent_schemas.md) | [AnyTypeMaxContainsValue](docs/components/schema/any_type_max_contains_value.md) | [AnyTypeMinContainsValue](docs/components/schema/any_type_min_contains_value.md) | +[AnyTypePatternProperties](docs/components/schema/any_type_pattern_properties.md) | [AnyTypePropertyNames](docs/components/schema/any_type_property_names.md) | [ArrayContainsValue](docs/components/schema/array_contains_value.md) | [ArrayMaxContainsValue](docs/components/schema/array_max_contains_value.md) | [ArrayMinContainsValue](docs/components/schema/array_min_contains_value.md) | [ObjectDependentRequired](docs/components/schema/object_dependent_required.md) | [ObjectDependentSchemas](docs/components/schema/object_dependent_schemas.md) | +[ObjectPatternProperties](docs/components/schema/object_pattern_properties.md) | [ObjectPropertyNames](docs/components/schema/object_property_names.md) | [StringConstString](docs/components/schema/string_const_string.md) | diff --git a/samples/client/3_1_0_json_schema/python/docs/components/schema/any_type_pattern_properties.md b/samples/client/3_1_0_json_schema/python/docs/components/schema/any_type_pattern_properties.md new file mode 100644 index 00000000000..47322265632 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/docs/components/schema/any_type_pattern_properties.md @@ -0,0 +1,12 @@ +# AnyTypePatternProperties +json_schema_api.components.schema.any_type_pattern_properties +``` +type: schemas.Schema +``` + +## validate method +Input Type | Return Type | Notes +------------ | ------------- | ------------- +dict, schemas.immutabledict, str, datetime.date, datetime.datetime, uuid.UUID, int, float, bool, None, list, tuple, bytes, io.FileIO, io.BufferedReader | schemas.immutabledict, str, float, int, bool, None, tuple, bytes, io.FileIO | + +[[Back to top]](#top) [[Back to Component Schemas]](../../../README.md#Component-Schemas) [[Back to README]](../../../README.md) diff --git a/samples/client/3_1_0_json_schema/python/docs/components/schema/object_pattern_properties.md b/samples/client/3_1_0_json_schema/python/docs/components/schema/object_pattern_properties.md new file mode 100644 index 00000000000..b49df8298a1 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/docs/components/schema/object_pattern_properties.md @@ -0,0 +1,12 @@ +# ObjectPatternProperties +json_schema_api.components.schema.object_pattern_properties +``` +type: schemas.Schema +``` + +## validate method +Input Type | Return Type | Notes +------------ | ------------- | ------------- +dict, schemas.immutabledict | schemas.immutabledict | + +[[Back to top]](#top) [[Back to Component Schemas]](../../../README.md#Component-Schemas) [[Back to README]](../../../README.md) diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/any_type_pattern_properties.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/any_type_pattern_properties.py new file mode 100644 index 00000000000..9b458d2d8d7 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/any_type_pattern_properties.py @@ -0,0 +1,40 @@ +# coding: utf-8 + +""" + Example + No description provided (generated by Openapi JSON Schema Generator https://github.com/openapi-json-schema-tools/openapi-json-schema-generator) # noqa: E501 + The version of the OpenAPI document: 1.0.0 + Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator +""" + +from __future__ import annotations +from json_schema_api.shared_imports.schema_imports import * # pyright: ignore [reportWildcardImportFromLibrary] + +S: typing_extensions.TypeAlias = schemas.StrSchema +I: typing_extensions.TypeAlias = schemas.IntSchema + + +@dataclasses.dataclass(frozen=True) +class AnyTypePatternProperties( + schemas.AnyTypeSchema[schemas.immutabledict[str, schemas.OUTPUT_BASE_TYPES], typing.Tuple[schemas.OUTPUT_BASE_TYPES, ...]], +): + """NOTE: This class is auto generated by OpenAPI JSON Schema Generator. + Ref: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator + + Do not edit the class manually. + """ + # any type + pattern_properties: typing.Mapping[ + schemas.PatternInfo, + typing.Type[schemas.Schema] + ] = dataclasses.field( + default_factory=lambda: { + schemas.PatternInfo( + pattern=r'^S_' # noqa: E501 + ): S, + schemas.PatternInfo( + pattern=r'^I_' # noqa: E501 + ): I, + } + ) + diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/object_pattern_properties.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/object_pattern_properties.py new file mode 100644 index 00000000000..41331b22b5c --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/object_pattern_properties.py @@ -0,0 +1,51 @@ +# coding: utf-8 + +""" + Example + No description provided (generated by Openapi JSON Schema Generator https://github.com/openapi-json-schema-tools/openapi-json-schema-generator) # noqa: E501 + The version of the OpenAPI document: 1.0.0 + Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator +""" + +from __future__ import annotations +from json_schema_api.shared_imports.schema_imports import * # pyright: ignore [reportWildcardImportFromLibrary] + +S: typing_extensions.TypeAlias = schemas.StrSchema +I: typing_extensions.TypeAlias = schemas.IntSchema + + +@dataclasses.dataclass(frozen=True) +class ObjectPatternProperties( + schemas.Schema[schemas.immutabledict[str, schemas.OUTPUT_BASE_TYPES], tuple] +): + """NOTE: This class is auto generated by OpenAPI JSON Schema Generator. + Ref: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator + + Do not edit the class manually. + """ + types: typing.FrozenSet[typing.Type] = frozenset({schemas.immutabledict}) + pattern_properties: typing.Mapping[ + schemas.PatternInfo, + typing.Type[schemas.Schema] + ] = dataclasses.field( + default_factory=lambda: { + schemas.PatternInfo( + pattern=r'^S_' # noqa: E501 + ): S, + schemas.PatternInfo( + pattern=r'^I_' # noqa: E501 + ): I, + } + ) + + @classmethod + def validate( + cls, + arg: typing.Mapping[str, schemas.INPUT_TYPES_ALL], + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> schemas.immutabledict[str, schemas.OUTPUT_BASE_TYPES]: + return super().validate_base( + arg, + configuration=configuration, + ) + diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schemas/__init__.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schemas/__init__.py index b29dce6d5e0..c6219a20569 100644 --- a/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schemas/__init__.py +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schemas/__init__.py @@ -17,11 +17,13 @@ from json_schema_api.components.schema.any_type_dependent_schemas import AnyTypeDependentSchemas from json_schema_api.components.schema.any_type_max_contains_value import AnyTypeMaxContainsValue from json_schema_api.components.schema.any_type_min_contains_value import AnyTypeMinContainsValue +from json_schema_api.components.schema.any_type_pattern_properties import AnyTypePatternProperties from json_schema_api.components.schema.any_type_property_names import AnyTypePropertyNames from json_schema_api.components.schema.array_contains_value import ArrayContainsValue from json_schema_api.components.schema.array_max_contains_value import ArrayMaxContainsValue from json_schema_api.components.schema.array_min_contains_value import ArrayMinContainsValue from json_schema_api.components.schema.object_dependent_required import ObjectDependentRequired from json_schema_api.components.schema.object_dependent_schemas import ObjectDependentSchemas +from json_schema_api.components.schema.object_pattern_properties import ObjectPatternProperties from json_schema_api.components.schema.object_property_names import ObjectPropertyNames from json_schema_api.components.schema.string_const_string import StringConstString diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/validation.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/validation.py index 79d685de88f..ca72e8603a9 100644 --- a/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/validation.py +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/validation.py @@ -1124,6 +1124,36 @@ def validate_property_names( return None +def validate_pattern_properties( + arg: typing.Any, + pattern_properties: typing.Mapping[PatternInfo, typing.Type[SchemaValidator]], + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> typing.Optional[PathToSchemasType]: + if not isinstance(arg, immutabledict): + return None + path_to_schemas: PathToSchemasType = {} + module_namespace = vars(sys.modules[cls.__module__]) + for property_name, property_value in arg.items(): + path_to_item = validation_metadata.path_to_item + (property_name,) + property_validation_metadata = ValidationMetadata( + path_to_item=path_to_item, + configuration=validation_metadata.configuration, + validated_path_to_schemas=validation_metadata.validated_path_to_schemas + ) + for pattern_info, schema in pattern_properties.items(): + flags = pattern_info.flags if pattern_info.flags is not None else 0 + if not re.search(pattern_info.pattern, property_name, flags=flags): + continue + schema = _get_class(schema, module_namespace) + if validation_metadata.validation_ran_earlier(schema): + add_deeper_validated_schemas(validation_metadata, path_to_schemas) + continue + other_path_to_schemas = schema._validate(property_value, validation_metadata=property_validation_metadata) + update(path_to_schemas, other_path_to_schemas) + return path_to_schemas + + validator_type = typing.Callable[[typing.Any, typing.Any, type, ValidationMetadata], typing.Optional[PathToSchemasType]] json_schema_keyword_to_validator: typing.Mapping[str, validator_type] = { 'types': validate_types, @@ -1157,5 +1187,6 @@ def validate_property_names( 'const_value_to_name': validate_const, 'dependent_required': validate_dependent_required, 'dependent_schemas': validate_dependent_schemas, - 'property_names': validate_property_names + 'property_names': validate_property_names, + 'pattern_properties': validate_pattern_properties } \ No newline at end of file diff --git a/samples/client/3_1_0_json_schema/python/test/components/schema/test_any_type_pattern_properties.py b/samples/client/3_1_0_json_schema/python/test/components/schema/test_any_type_pattern_properties.py new file mode 100644 index 00000000000..024648c1ab4 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/test/components/schema/test_any_type_pattern_properties.py @@ -0,0 +1,41 @@ +# coding: utf-8 + +""" + Example + No description provided (generated by Openapi JSON Schema Generator https://github.com/openapi-json-schema-tools/openapi-json-schema-generator) # noqa: E501 + The version of the OpenAPI document: 1.0.0 + Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator +""" + +import unittest + +import json_schema_api +from json_schema_api.components.schema.any_type_pattern_properties import AnyTypePatternProperties + + +class TestAnyTypePatternProperties(unittest.TestCase): + """AnyTypePatternProperties unit test stubs""" + + def test_succceds_with_other_type(self): + inst = AnyTypePatternProperties.validate(0) + assert inst == 0 + + def test_succceds_with_non_matching_key(self): + inst = AnyTypePatternProperties.validate({"keyword": "value"}) + assert inst == {"keyword": "value"} + + def test_succceds_with_matching_keys(self): + inst = AnyTypePatternProperties.validate({"S_25": "This is a string", "I_0": 42}) + assert inst == {"S_25": "This is a string", "I_0": 42} + + def test_fails_with_incorrect_value_type_for_s_key(self): + with self.assertRaises(json_schema_api.ApiTypeError): + AnyTypePatternProperties.validate({"S_0": 42}) + + def test_fails_with_incorrect_value_type_for_i_key(self): + with self.assertRaises(json_schema_api.ApiTypeError): + AnyTypePatternProperties.validate({"I_0": "This is a string"}) + + +if __name__ == '__main__': + unittest.main() diff --git a/samples/client/3_1_0_json_schema/python/test/components/schema/test_object_pattern_properties.py b/samples/client/3_1_0_json_schema/python/test/components/schema/test_object_pattern_properties.py new file mode 100644 index 00000000000..d5315da060e --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/test/components/schema/test_object_pattern_properties.py @@ -0,0 +1,37 @@ +# coding: utf-8 + +""" + Example + No description provided (generated by Openapi JSON Schema Generator https://github.com/openapi-json-schema-tools/openapi-json-schema-generator) # noqa: E501 + The version of the OpenAPI document: 1.0.0 + Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator +""" + +import unittest + +import json_schema_api +from json_schema_api.components.schema.object_pattern_properties import ObjectPatternProperties + + +class TestObjectPatternProperties(unittest.TestCase): + """ObjectPatternProperties unit test stubs""" + + def test_succceds_with_non_matching_key(self): + inst = ObjectPatternProperties.validate({"keyword": "value"}) + assert inst == {"keyword": "value"} + + def test_succceds_with_matching_keys(self): + inst = ObjectPatternProperties.validate({"S_25": "This is a string", "I_0": 42}) + assert inst == {"S_25": "This is a string", "I_0": 42} + + def test_fails_with_incorrect_value_type_for_s_key(self): + with self.assertRaises(json_schema_api.ApiTypeError): + ObjectPatternProperties.validate({"S_0": 42}) + + def test_fails_with_incorrect_value_type_for_i_key(self): + with self.assertRaises(json_schema_api.ApiTypeError): + ObjectPatternProperties.validate({"I_0": "This is a string"}) + + +if __name__ == '__main__': + unittest.main() diff --git a/samples/client/openapi_features/nonCompliantUseDiscriminatorIfCompositionFails/python/src/this_package/schemas/validation.py b/samples/client/openapi_features/nonCompliantUseDiscriminatorIfCompositionFails/python/src/this_package/schemas/validation.py index 45be29d5490..c4dc0fa1d9c 100644 --- a/samples/client/openapi_features/nonCompliantUseDiscriminatorIfCompositionFails/python/src/this_package/schemas/validation.py +++ b/samples/client/openapi_features/nonCompliantUseDiscriminatorIfCompositionFails/python/src/this_package/schemas/validation.py @@ -1208,6 +1208,37 @@ def validate_property_names( return None +def validate_pattern_properties( + arg: typing.Any, + pattern_properties: typing.Mapping[PatternInfo, typing.Type[SchemaValidator]], + cls: typing.Type, + validation_metadata: ValidationMetadata, + **kwargs +) -> typing.Optional[PathToSchemasType]: + if not isinstance(arg, immutabledict): + return None + path_to_schemas: PathToSchemasType = {} + module_namespace = vars(sys.modules[cls.__module__]) + for property_name, property_value in arg.items(): + path_to_item = validation_metadata.path_to_item + (property_name,) + property_validation_metadata = ValidationMetadata( + path_to_item=path_to_item, + configuration=validation_metadata.configuration, + validated_path_to_schemas=validation_metadata.validated_path_to_schemas + ) + for pattern_info, schema in pattern_properties.items(): + flags = pattern_info.flags if pattern_info.flags is not None else 0 + if not re.search(pattern_info.pattern, property_name, flags=flags): + continue + schema = _get_class(schema, module_namespace) + if validation_metadata.validation_ran_earlier(schema): + add_deeper_validated_schemas(validation_metadata, path_to_schemas) + continue + other_path_to_schemas = schema._validate(property_value, validation_metadata=property_validation_metadata) + update(path_to_schemas, other_path_to_schemas) + return path_to_schemas + + validator_type = typing.Callable[[typing.Any, typing.Any, type, ValidationMetadata], typing.Optional[PathToSchemasType]] json_schema_keyword_to_validator: typing.Mapping[str, validator_type] = { 'types': validate_types, @@ -1241,5 +1272,6 @@ def validate_property_names( 'const_value_to_name': validate_const, 'dependent_required': validate_dependent_required, 'dependent_schemas': validate_dependent_schemas, - 'property_names': validate_property_names + 'property_names': validate_property_names, + 'pattern_properties': validate_pattern_properties } \ No newline at end of file diff --git a/samples/client/openapi_features/security/python/src/this_package/schemas/validation.py b/samples/client/openapi_features/security/python/src/this_package/schemas/validation.py index 7ad4a481257..cd5c877dcec 100644 --- a/samples/client/openapi_features/security/python/src/this_package/schemas/validation.py +++ b/samples/client/openapi_features/security/python/src/this_package/schemas/validation.py @@ -1124,6 +1124,36 @@ def validate_property_names( return None +def validate_pattern_properties( + arg: typing.Any, + pattern_properties: typing.Mapping[PatternInfo, typing.Type[SchemaValidator]], + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> typing.Optional[PathToSchemasType]: + if not isinstance(arg, immutabledict): + return None + path_to_schemas: PathToSchemasType = {} + module_namespace = vars(sys.modules[cls.__module__]) + for property_name, property_value in arg.items(): + path_to_item = validation_metadata.path_to_item + (property_name,) + property_validation_metadata = ValidationMetadata( + path_to_item=path_to_item, + configuration=validation_metadata.configuration, + validated_path_to_schemas=validation_metadata.validated_path_to_schemas + ) + for pattern_info, schema in pattern_properties.items(): + flags = pattern_info.flags if pattern_info.flags is not None else 0 + if not re.search(pattern_info.pattern, property_name, flags=flags): + continue + schema = _get_class(schema, module_namespace) + if validation_metadata.validation_ran_earlier(schema): + add_deeper_validated_schemas(validation_metadata, path_to_schemas) + continue + other_path_to_schemas = schema._validate(property_value, validation_metadata=property_validation_metadata) + update(path_to_schemas, other_path_to_schemas) + return path_to_schemas + + validator_type = typing.Callable[[typing.Any, typing.Any, type, ValidationMetadata], typing.Optional[PathToSchemasType]] json_schema_keyword_to_validator: typing.Mapping[str, validator_type] = { 'types': validate_types, @@ -1157,5 +1187,6 @@ def validate_property_names( 'const_value_to_name': validate_const, 'dependent_required': validate_dependent_required, 'dependent_schemas': validate_dependent_schemas, - 'property_names': validate_property_names + 'property_names': validate_property_names, + 'pattern_properties': validate_pattern_properties } \ No newline at end of file diff --git a/samples/client/petstore/python/src/petstore_api/schemas/validation.py b/samples/client/petstore/python/src/petstore_api/schemas/validation.py index eaebc061ca9..c367436bed8 100644 --- a/samples/client/petstore/python/src/petstore_api/schemas/validation.py +++ b/samples/client/petstore/python/src/petstore_api/schemas/validation.py @@ -1124,6 +1124,36 @@ def validate_property_names( return None +def validate_pattern_properties( + arg: typing.Any, + pattern_properties: typing.Mapping[PatternInfo, typing.Type[SchemaValidator]], + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> typing.Optional[PathToSchemasType]: + if not isinstance(arg, immutabledict): + return None + path_to_schemas: PathToSchemasType = {} + module_namespace = vars(sys.modules[cls.__module__]) + for property_name, property_value in arg.items(): + path_to_item = validation_metadata.path_to_item + (property_name,) + property_validation_metadata = ValidationMetadata( + path_to_item=path_to_item, + configuration=validation_metadata.configuration, + validated_path_to_schemas=validation_metadata.validated_path_to_schemas + ) + for pattern_info, schema in pattern_properties.items(): + flags = pattern_info.flags if pattern_info.flags is not None else 0 + if not re.search(pattern_info.pattern, property_name, flags=flags): + continue + schema = _get_class(schema, module_namespace) + if validation_metadata.validation_ran_earlier(schema): + add_deeper_validated_schemas(validation_metadata, path_to_schemas) + continue + other_path_to_schemas = schema._validate(property_value, validation_metadata=property_validation_metadata) + update(path_to_schemas, other_path_to_schemas) + return path_to_schemas + + validator_type = typing.Callable[[typing.Any, typing.Any, type, ValidationMetadata], typing.Optional[PathToSchemasType]] json_schema_keyword_to_validator: typing.Mapping[str, validator_type] = { 'types': validate_types, @@ -1157,5 +1187,6 @@ def validate_property_names( 'const_value_to_name': validate_const, 'dependent_required': validate_dependent_required, 'dependent_schemas': validate_dependent_schemas, - 'property_names': validate_property_names + 'property_names': validate_property_names, + 'pattern_properties': validate_pattern_properties } \ No newline at end of file diff --git a/src/main/java/org/openapijsonschematools/codegen/generators/DefaultGenerator.java b/src/main/java/org/openapijsonschematools/codegen/generators/DefaultGenerator.java index 16b1bc93b11..20fb12572f8 100644 --- a/src/main/java/org/openapijsonschematools/codegen/generators/DefaultGenerator.java +++ b/src/main/java/org/openapijsonschematools/codegen/generators/DefaultGenerator.java @@ -2217,6 +2217,18 @@ private String schemaPathFromDocRoot(String moduleLocation) { return moduleLocation.replace('.', File.separatorChar).substring(packageName.length()+1); } + private LinkedHashMap getPatternProperties(Map schemaPatternProperties, String jsonPath, String sourceJsonPath) { + LinkedHashMap patternProperties = new LinkedHashMap<>(); + for (Entry entry: schemaPatternProperties.entrySet()) { + String pattern = entry.getKey(); + CodegenPatternInfo patternInfo = getPatternInfo(pattern); + CodegenSchema schema = fromSchema(entry.getValue(), sourceJsonPath, jsonPath + "/patternProperties/" + ModelUtils.encodeSlashes(pattern)); + patternProperties.put(patternInfo, schema); + } + return patternProperties; + } + + /** * Convert OAS Property object to Codegen Property object. *

@@ -2297,6 +2309,7 @@ public CodegenSchema fromSchema(Schema p, String sourceJsonPath, String currentJ (self) propertyNames properties + patternProperties oneOf not items @@ -2311,6 +2324,9 @@ public CodegenSchema fromSchema(Schema p, String sourceJsonPath, String currentJ setSchemaLocationInfo(null, sourceJsonPath, currentJsonPath, property); HashMap requiredAndOptionalProperties = new HashMap<>(); property.properties = getProperties(((Schema) p).getProperties(), sourceJsonPath, currentJsonPath, requiredAndOptionalProperties); + if (p.getPatternProperties() != null) { + property.patternProperties = getPatternProperties(p.getPatternProperties(), currentJsonPath, sourceJsonPath); + } LinkedHashSet required = p.getRequired() == null ? new LinkedHashSet<>() : new LinkedHashSet<>(((Schema) p).getRequired()); List oneOfs = ((Schema) p).getOneOf(); diff --git a/src/main/java/org/openapijsonschematools/codegen/generators/PythonClientGenerator.java b/src/main/java/org/openapijsonschematools/codegen/generators/PythonClientGenerator.java index 11e50ebb48b..142b6f887b0 100644 --- a/src/main/java/org/openapijsonschematools/codegen/generators/PythonClientGenerator.java +++ b/src/main/java/org/openapijsonschematools/codegen/generators/PythonClientGenerator.java @@ -165,6 +165,7 @@ public PythonClientGenerator() { SchemaFeature.Nullable, SchemaFeature.OneOf, SchemaFeature.Pattern, + SchemaFeature.PatternProperties, SchemaFeature.Properties, SchemaFeature.PropertyNames, SchemaFeature.Required, diff --git a/src/main/java/org/openapijsonschematools/codegen/generators/generatormetadata/features/SchemaFeature.java b/src/main/java/org/openapijsonschematools/codegen/generators/generatormetadata/features/SchemaFeature.java index 7cea56fa78f..717c3fbbe16 100644 --- a/src/main/java/org/openapijsonschematools/codegen/generators/generatormetadata/features/SchemaFeature.java +++ b/src/main/java/org/openapijsonschematools/codegen/generators/generatormetadata/features/SchemaFeature.java @@ -118,6 +118,9 @@ public enum SchemaFeature { @OAS2 @OAS3 Pattern, + @OAS3 + PatternProperties, + @OAS2 @OAS3 Properties, diff --git a/src/main/java/org/openapijsonschematools/codegen/generators/openapimodels/CodegenSchema.java b/src/main/java/org/openapijsonschematools/codegen/generators/openapimodels/CodegenSchema.java index 4d52fe908ca..8beda7f2b2c 100644 --- a/src/main/java/org/openapijsonschematools/codegen/generators/openapimodels/CodegenSchema.java +++ b/src/main/java/org/openapijsonschematools/codegen/generators/openapimodels/CodegenSchema.java @@ -84,6 +84,7 @@ public class CodegenSchema { public boolean isBooleanSchemaFalse; // supports boolean schemas public EnumInfo constInfo; public CodegenSchema propertyNames; + public LinkedHashMap patternProperties; // Extra needed fields // stores the mapping value schema, used to provide a value type for the object output class @@ -253,6 +254,7 @@ private void getAllSchemas(ArrayList schemasBeforeImports, ArrayL items not oneOf + patternProperties properties propertyNames (self) @@ -374,6 +376,11 @@ private void getAllSchemas(ArrayList schemasBeforeImports, ArrayL schemasAfterImports.add(extraSchema); } } + if (patternProperties != null) { + for (CodegenSchema someSchema: patternProperties.values()) { + someSchema.getAllSchemas(schemasBeforeImports, schemasAfterImports, level + 1); + } + } if (properties != null) { for (CodegenSchema someSchema: properties.values()) { someSchema.getAllSchemas(schemasBeforeImports, schemasAfterImports, level + 1); diff --git a/src/main/resources/python/components/schemas/schema_cls/__pattern_info.hbs b/src/main/resources/python/components/schemas/schema_cls/__pattern_info.hbs new file mode 100644 index 00000000000..ed5deb2358f --- /dev/null +++ b/src/main/resources/python/components/schemas/schema_cls/__pattern_info.hbs @@ -0,0 +1,31 @@ +{{#if propertyName}}{{propertyName}}: schemas.PatternInfo = {{/if}}schemas.PatternInfo( +{{#with patternInfo}} + pattern=r'{{{pattern}}}'{{#if flags}},{{/if}} # noqa: E501 + {{#if flags}} + {{#eq flags.size 1}} + flags={{#each flags}}re.{{#eq this "i"}}I{{/eq}}{{#eq this "m"}}M{{/eq}}{{#eq this "s"}}S{{/eq}}{{#eq this "u"}}U{{/eq}}{{/each}}, + {{else}} + flags=( + {{#each flags}} + {{#eq this "i"}} + re.I{{#unless @last}} |{{/unless}} + {{/eq}} + {{#eq this "m"}} + re.M{{#unless @last}} |{{/unless}} + {{/eq}} + {{#eq this "s"}} + re.S{{#unless @last}} |{{/unless}} + {{/eq}} + {{#eq this "u"}} + re.U{{#unless @last}} |{{/unless}} + {{/eq}} + {{/each}} + ) + {{/eq}} + {{/if}} +{{/with}} +{{#if jsonPathPiece}} +): {{jsonPathPiece.camelCase}}, +{{else}} +) +{{/if}} \ No newline at end of file diff --git a/src/main/resources/python/components/schemas/schema_cls/_pattern.hbs b/src/main/resources/python/components/schemas/schema_cls/_pattern.hbs index ec188c63ac3..5b2a0735c3b 100644 --- a/src/main/resources/python/components/schemas/schema_cls/_pattern.hbs +++ b/src/main/resources/python/components/schemas/schema_cls/_pattern.hbs @@ -1,27 +1 @@ -pattern: schemas.PatternInfo = schemas.PatternInfo( - {{#with patternInfo}} - pattern=r'{{{pattern}}}'{{#if flags}},{{/if}} # noqa: E501 - {{#if flags}} - {{#eq flags.size 1}} - flags={{#each flags}}re.{{#eq this "i"}}I{{/eq}}{{#eq this "m"}}M{{/eq}}{{#eq this "s"}}S{{/eq}}{{#eq this "u"}}U{{/eq}}{{/each}}, - {{else}} - flags=( - {{#each flags}} - {{#eq this "i"}} - re.I{{#unless @last}} |{{/unless}} - {{/eq}} - {{#eq this "m"}} - re.M{{#unless @last}} |{{/unless}} - {{/eq}} - {{#eq this "s"}} - re.S{{#unless @last}} |{{/unless}} - {{/eq}} - {{#eq this "u"}} - re.U{{#unless @last}} |{{/unless}} - {{/eq}} - {{/each}} - ) - {{/eq}} - {{/if}} - {{/with}} -) +{{> components/schemas/schema_cls/__pattern_info propertyName="pattern" jsonPathPiece=false }} diff --git a/src/main/resources/python/components/schemas/schema_cls/_pattern_properties.hbs b/src/main/resources/python/components/schemas/schema_cls/_pattern_properties.hbs new file mode 100644 index 00000000000..2217e1c2d0a --- /dev/null +++ b/src/main/resources/python/components/schemas/schema_cls/_pattern_properties.hbs @@ -0,0 +1,10 @@ +pattern_properties: typing.Mapping[ + schemas.PatternInfo, + typing.Type[schemas.Schema] +] = dataclasses.field( + default_factory=lambda: { +{{#each patternProperties}} + {{> components/schemas/schema_cls/__pattern_info propertyName=false patternInfo=@key }} +{{/each}} + } +) diff --git a/src/main/resources/python/components/schemas/schema_cls/_schema_anytype_or_multitype.hbs b/src/main/resources/python/components/schemas/schema_cls/_schema_anytype_or_multitype.hbs index 13c16efa8ba..25055f899dd 100644 --- a/src/main/resources/python/components/schemas/schema_cls/_schema_anytype_or_multitype.hbs +++ b/src/main/resources/python/components/schemas/schema_cls/_schema_anytype_or_multitype.hbs @@ -108,6 +108,9 @@ class {{jsonPathPiece.camelCase}}( {{#if propertyNames}} {{> components/schemas/schema_cls/_property_names }} {{/if}} +{{#if patternProperties}} + {{> components/schemas/schema_cls/_pattern_properties }} +{{/if}} {{#or mapOutputJsonPathPiece arrayOutputJsonPathPiece}} type_to_output_cls: typing.Mapping[ typing.Type, diff --git a/src/main/resources/python/components/schemas/schema_cls/_schema_dict.hbs b/src/main/resources/python/components/schemas/schema_cls/_schema_dict.hbs index ff9a4dda68f..637810bd6b6 100644 --- a/src/main/resources/python/components/schemas/schema_cls/_schema_dict.hbs +++ b/src/main/resources/python/components/schemas/schema_cls/_schema_dict.hbs @@ -55,6 +55,9 @@ class {{jsonPathPiece.camelCase}}( {{#if propertyNames}} {{> components/schemas/schema_cls/_property_names }} {{/if}} + {{#if patternProperties}} + {{> components/schemas/schema_cls/_pattern_properties }} + {{/if}} {{#if mapOutputJsonPathPiece}} type_to_output_cls: typing.Mapping[ typing.Type, diff --git a/src/main/resources/python/components/schemas/schema_cls/schema_cls.hbs b/src/main/resources/python/components/schemas/schema_cls/schema_cls.hbs index dfde24cafbe..f10eeecb80d 100644 --- a/src/main/resources/python/components/schemas/schema_cls/schema_cls.hbs +++ b/src/main/resources/python/components/schemas/schema_cls/schema_cls.hbs @@ -2,7 +2,7 @@ {{> components/schemas/schema_cls/_schema_var_equals_cls }} {{else}} {{#eq types null }} - {{#or allOf anyOf oneOf not enumInfo constInfo properties requiredProperties hasDiscriminatorWithNonEmptyMapping additionalProperties dependentRequired dependentSchemas propertyNames (neq maxProperties null) (neq minProperties null) items uniqueItems (neq maxItems null) (neq minItems null) contains (neq maxContains null) (neq minContains null) format (neq maxLength null) (neq minLength null) maximum minimum multipleOf patternInfo }} + {{#or allOf anyOf oneOf not enumInfo constInfo properties requiredProperties hasDiscriminatorWithNonEmptyMapping additionalProperties dependentRequired dependentSchemas propertyNames (neq maxProperties null) (neq minProperties null) patternProperties items uniqueItems (neq maxItems null) (neq minItems null) contains (neq maxContains null) (neq minContains null) format (neq maxLength null) (neq minLength null) maximum minimum multipleOf patternInfo }} {{> components/schemas/schema_cls/_schema_anytype_or_multitype }} {{else}} {{> components/schemas/schema_cls/_schema_var_equals_cls }} @@ -13,7 +13,7 @@ {{! one type }} {{#each types}} {{#eq this "object"}} - {{#or allOf anyOf oneOf not properties requiredProperties hasDiscriminatorWithNonEmptyMapping additionalProperties dependentRequired dependentSchemas propertyNames maxProperties minProperties }} + {{#or allOf anyOf oneOf not properties requiredProperties hasDiscriminatorWithNonEmptyMapping additionalProperties dependentRequired dependentSchemas propertyNames (neq maxProperties null) (neq minProperties null) patternProperties }} {{> components/schemas/schema_cls/_schema_dict }} {{else}} {{> components/schemas/schema_cls/_schema_var_equals_cls }} diff --git a/src/main/resources/python/schemas/validation.hbs b/src/main/resources/python/schemas/validation.hbs index 4020b7203ac..f85878fc9ef 100644 --- a/src/main/resources/python/schemas/validation.hbs +++ b/src/main/resources/python/schemas/validation.hbs @@ -1286,6 +1286,39 @@ def validate_property_names( return None +def validate_pattern_properties( + arg: typing.Any, + pattern_properties: typing.Mapping[PatternInfo, typing.Type[SchemaValidator]], + cls: typing.Type, + validation_metadata: ValidationMetadata, +{{#if nonCompliantUseDiscriminatorIfCompositionFails}} + **kwargs +{{/if}} +) -> typing.Optional[PathToSchemasType]: + if not isinstance(arg, immutabledict): + return None + path_to_schemas: PathToSchemasType = {} + module_namespace = vars(sys.modules[cls.__module__]) + for property_name, property_value in arg.items(): + path_to_item = validation_metadata.path_to_item + (property_name,) + property_validation_metadata = ValidationMetadata( + path_to_item=path_to_item, + configuration=validation_metadata.configuration, + validated_path_to_schemas=validation_metadata.validated_path_to_schemas + ) + for pattern_info, schema in pattern_properties.items(): + flags = pattern_info.flags if pattern_info.flags is not None else 0 + if not re.search(pattern_info.pattern, property_name, flags=flags): + continue + schema = _get_class(schema, module_namespace) + if validation_metadata.validation_ran_earlier(schema): + add_deeper_validated_schemas(validation_metadata, path_to_schemas) + continue + other_path_to_schemas = schema._validate(property_value, validation_metadata=property_validation_metadata) + update(path_to_schemas, other_path_to_schemas) + return path_to_schemas + + validator_type = typing.Callable[[typing.Any, typing.Any, type, ValidationMetadata], typing.Optional[PathToSchemasType]] json_schema_keyword_to_validator: typing.Mapping[str, validator_type] = { 'types': validate_types, @@ -1319,5 +1352,6 @@ json_schema_keyword_to_validator: typing.Mapping[str, validator_type] = { 'const_value_to_name': validate_const, 'dependent_required': validate_dependent_required, 'dependent_schemas': validate_dependent_schemas, - 'property_names': validate_property_names + 'property_names': validate_property_names, + 'pattern_properties': validate_pattern_properties } \ No newline at end of file diff --git a/src/test/resources/3_1/json_schema.yaml b/src/test/resources/3_1/json_schema.yaml index c22da01b6c3..8a09d4b2bac 100644 --- a/src/test/resources/3_1/json_schema.yaml +++ b/src/test/resources/3_1/json_schema.yaml @@ -82,4 +82,17 @@ components: ObjectPropertyNames: type: object propertyNames: - pattern: "^[A-Za-z_][A-Za-z0-9_]*$" \ No newline at end of file + pattern: "^[A-Za-z_][A-Za-z0-9_]*$" + AnyTypePatternProperties: + patternProperties: + "^S_": + type: string + "^I_": + type: integer + ObjectPatternProperties: + type: object + patternProperties: + "^S_": + type: string + "^I_": + type: integer