forked from loafle/openapi-generator-original
Python-exp remove codegemodel mutation, allow mixed OneOf types (#6797)
* Stops converting primitive models into object models, adds ComposedSchemas with mixed type * Samples update for python-exp
This commit is contained in:
@@ -62,6 +62,58 @@ class cached_property(object):
|
||||
return result
|
||||
|
||||
|
||||
PRIMITIVE_TYPES = (list, float, int, bool, datetime, date, str, file_type)
|
||||
|
||||
def allows_single_value_input(cls):
|
||||
"""
|
||||
This function returns True if the input composed schema model or any
|
||||
descendant model allows a value only input
|
||||
This is true for cases where oneOf contains items like:
|
||||
oneOf:
|
||||
- float
|
||||
- NumberWithValidation
|
||||
- StringEnum
|
||||
- ArrayModel
|
||||
- null
|
||||
TODO: lru_cache this
|
||||
"""
|
||||
if (
|
||||
issubclass(cls, ModelSimple) or
|
||||
cls in PRIMITIVE_TYPES
|
||||
):
|
||||
return True
|
||||
elif issubclass(cls, ModelComposed):
|
||||
if not cls._composed_schemas['oneOf']:
|
||||
return False
|
||||
return any(allows_single_value_input(c) for c in cls._composed_schemas['oneOf'])
|
||||
return False
|
||||
|
||||
def composed_model_input_classes(cls):
|
||||
"""
|
||||
This function returns a list of the possible models that can be accepted as
|
||||
inputs.
|
||||
TODO: lru_cache this
|
||||
"""
|
||||
if issubclass(cls, ModelSimple) or cls in PRIMITIVE_TYPES:
|
||||
return [cls]
|
||||
elif issubclass(cls, ModelNormal):
|
||||
if cls.discriminator is None:
|
||||
return [cls]
|
||||
else:
|
||||
return get_discriminated_classes(cls)
|
||||
elif issubclass(cls, ModelComposed):
|
||||
if not cls._composed_schemas['oneOf']:
|
||||
return []
|
||||
if cls.discriminator is None:
|
||||
input_classes = []
|
||||
for c in cls._composed_schemas['oneOf']:
|
||||
input_classes.extend(composed_model_input_classes(c))
|
||||
return input_classes
|
||||
else:
|
||||
return get_discriminated_classes(cls)
|
||||
return []
|
||||
|
||||
|
||||
class OpenApiModel(object):
|
||||
"""The base class for all OpenAPIModels"""
|
||||
|
||||
@@ -138,9 +190,17 @@ class OpenApiModel(object):
|
||||
# pick a new schema/class to instantiate because a discriminator
|
||||
# propertyName value was passed in
|
||||
|
||||
if len(args) == 1 and args[0] is None and is_type_nullable(cls):
|
||||
# The input data is the 'null' value and the type is nullable.
|
||||
return None
|
||||
if len(args) == 1:
|
||||
arg = args[0]
|
||||
if arg is None and is_type_nullable(cls):
|
||||
# The input data is the 'null' value and the type is nullable.
|
||||
return None
|
||||
|
||||
if issubclass(cls, ModelComposed) and allows_single_value_input(cls):
|
||||
model_kwargs = {}
|
||||
oneof_instance = get_oneof_instance(cls, model_kwargs, kwargs, model_arg=arg)
|
||||
return oneof_instance
|
||||
|
||||
|
||||
visited_composed_classes = kwargs.get('_visited_composed_classes', ())
|
||||
if (
|
||||
@@ -508,6 +568,10 @@ UPCONVERSION_TYPE_PAIRS = (
|
||||
(int, float), # A float may be serialized as an integer, e.g. '3' is a valid serialized float.
|
||||
(list, ModelComposed),
|
||||
(dict, ModelComposed),
|
||||
(str, ModelComposed),
|
||||
(int, ModelComposed),
|
||||
(float, ModelComposed),
|
||||
(list, ModelComposed),
|
||||
(list, ModelNormal),
|
||||
(dict, ModelNormal),
|
||||
(str, ModelSimple),
|
||||
@@ -886,20 +950,53 @@ def remove_uncoercible(required_types_classes, current_item, spec_property_namin
|
||||
results_classes.append(required_type_class)
|
||||
return results_classes
|
||||
|
||||
def get_discriminated_classes(cls):
|
||||
"""
|
||||
Returns all the classes that a discriminator converts to
|
||||
TODO: lru_cache this
|
||||
"""
|
||||
possible_classes = []
|
||||
key = list(cls.discriminator.keys())[0]
|
||||
if is_type_nullable(cls):
|
||||
possible_classes.append(cls)
|
||||
for discr_cls in cls.discriminator[key].values():
|
||||
if hasattr(discr_cls, 'discriminator') and discr_cls.discriminator is not None:
|
||||
possible_classes.extend(get_discriminated_classes(discr_cls))
|
||||
else:
|
||||
possible_classes.append(discr_cls)
|
||||
return possible_classes
|
||||
|
||||
def get_required_type_classes(required_types_mixed):
|
||||
|
||||
def get_possible_classes(cls, from_server_context):
|
||||
# TODO: lru_cache this
|
||||
possible_classes = [cls]
|
||||
if from_server_context:
|
||||
return possible_classes
|
||||
if hasattr(cls, 'discriminator') and cls.discriminator is not None:
|
||||
possible_classes = []
|
||||
possible_classes.extend(get_discriminated_classes(cls))
|
||||
elif issubclass(cls, ModelComposed):
|
||||
possible_classes.extend(composed_model_input_classes(cls))
|
||||
return possible_classes
|
||||
|
||||
|
||||
def get_required_type_classes(required_types_mixed, spec_property_naming):
|
||||
"""Converts the tuple required_types into a tuple and a dict described
|
||||
below
|
||||
|
||||
Args:
|
||||
required_types_mixed (tuple/list): will contain either classes or
|
||||
instance of list or dict
|
||||
spec_property_naming (bool): if True these values came from the
|
||||
server, and we use the data types in our endpoints.
|
||||
If False, we are client side and we need to include
|
||||
oneOf and discriminator classes inside the data types in our endpoints
|
||||
|
||||
Returns:
|
||||
(valid_classes, dict_valid_class_to_child_types_mixed):
|
||||
valid_classes (tuple): the valid classes that the current item
|
||||
should be
|
||||
dict_valid_class_to_child_types_mixed (doct):
|
||||
dict_valid_class_to_child_types_mixed (dict):
|
||||
valid_class (class): this is the key
|
||||
child_types_mixed (list/dict/tuple): describes the valid child
|
||||
types
|
||||
@@ -917,7 +1014,7 @@ def get_required_type_classes(required_types_mixed):
|
||||
valid_classes.append(dict)
|
||||
child_req_types_by_current_type[dict] = required_type[str]
|
||||
else:
|
||||
valid_classes.append(required_type)
|
||||
valid_classes.extend(get_possible_classes(required_type, spec_property_naming))
|
||||
return tuple(valid_classes), child_req_types_by_current_type
|
||||
|
||||
|
||||
@@ -1070,7 +1167,7 @@ def deserialize_model(model_data, model_class, path_to_item, check_type,
|
||||
"""Deserializes model_data to model instance.
|
||||
|
||||
Args:
|
||||
model_data (list/dict): data to instantiate the model
|
||||
model_data (int/str/float/bool/none_type/list/dict): data to instantiate the model
|
||||
model_class (OpenApiModel): the model class
|
||||
path_to_item (list): path to the model in the received data
|
||||
check_type (bool): whether to check the data tupe for the values in
|
||||
@@ -1096,14 +1193,14 @@ def deserialize_model(model_data, model_class, path_to_item, check_type,
|
||||
_spec_property_naming=spec_property_naming)
|
||||
|
||||
if issubclass(model_class, ModelSimple):
|
||||
instance = model_class(value=model_data, **kw_args)
|
||||
return instance
|
||||
if isinstance(model_data, list):
|
||||
instance = model_class(*model_data, **kw_args)
|
||||
return model_class(model_data, **kw_args)
|
||||
elif isinstance(model_data, list):
|
||||
return model_class(*model_data, **kw_args)
|
||||
if isinstance(model_data, dict):
|
||||
kw_args.update(model_data)
|
||||
instance = model_class(**kw_args)
|
||||
return instance
|
||||
return model_class(**kw_args)
|
||||
elif isinstance(model_data, PRIMITIVE_TYPES):
|
||||
return model_class(model_data, **kw_args)
|
||||
|
||||
|
||||
def deserialize_file(response_data, configuration, content_disposition=None):
|
||||
@@ -1286,7 +1383,7 @@ def validate_and_convert_types(input_value, required_types_mixed, path_to_item,
|
||||
Raises:
|
||||
ApiTypeError
|
||||
"""
|
||||
results = get_required_type_classes(required_types_mixed)
|
||||
results = get_required_type_classes(required_types_mixed, spec_property_naming)
|
||||
valid_classes, child_req_types_by_current_type = results
|
||||
|
||||
input_class_simple = get_simple_class(input_value)
|
||||
@@ -1523,7 +1620,7 @@ def get_allof_instances(self, model_args, constant_args):
|
||||
return composed_instances
|
||||
|
||||
|
||||
def get_oneof_instance(self, model_args, constant_args):
|
||||
def get_oneof_instance(cls, model_kwargs, constant_kwargs, model_arg=None):
|
||||
"""
|
||||
Find the oneOf schema that matches the input data (e.g. payload).
|
||||
If exactly one schema matches the input data, an instance of that schema
|
||||
@@ -1531,25 +1628,33 @@ def get_oneof_instance(self, model_args, constant_args):
|
||||
If zero or more than one schema match the input data, an exception is raised.
|
||||
In OAS 3.x, the payload MUST, by validation, match exactly one of the
|
||||
schemas described by oneOf.
|
||||
|
||||
Args:
|
||||
self: the class we are handling
|
||||
model_args (dict): var_name to var_value
|
||||
cls: the class we are handling
|
||||
model_kwargs (dict): var_name to var_value
|
||||
The input data, e.g. the payload that must match a oneOf schema
|
||||
in the OpenAPI document.
|
||||
constant_args (dict): var_name to var_value
|
||||
constant_kwargs (dict): var_name to var_value
|
||||
args that every model requires, including configuration, server
|
||||
and path to item.
|
||||
|
||||
Kwargs:
|
||||
model_arg: (int, float, bool, str, date, datetime, ModelSimple, None):
|
||||
the value to assign to a primitive class or ModelSimple class
|
||||
Notes:
|
||||
- this is only passed in when oneOf includes types which are not object
|
||||
- None is used to suppress handling of model_arg, nullable models are handled in __new__
|
||||
|
||||
Returns
|
||||
oneof_instance (instance)
|
||||
"""
|
||||
if len(self._composed_schemas['oneOf']) == 0:
|
||||
if len(cls._composed_schemas['oneOf']) == 0:
|
||||
return None
|
||||
|
||||
oneof_instances = []
|
||||
# Iterate over each oneOf schema and determine if the input data
|
||||
# matches the oneOf schemas.
|
||||
for oneof_class in self._composed_schemas['oneOf']:
|
||||
for oneof_class in cls._composed_schemas['oneOf']:
|
||||
# The composed oneOf schema allows the 'null' type and the input data
|
||||
# is the null value. This is a OAS >= 3.1 feature.
|
||||
if oneof_class is none_type:
|
||||
@@ -1557,28 +1662,45 @@ def get_oneof_instance(self, model_args, constant_args):
|
||||
# none_type deserialization is handled in the __new__ method
|
||||
continue
|
||||
|
||||
# transform js keys from input data to python keys in fixed_model_args
|
||||
fixed_model_args = change_keys_js_to_python(
|
||||
model_args, oneof_class)
|
||||
single_value_input = allows_single_value_input(oneof_class)
|
||||
|
||||
# Extract a dict with the properties that are declared in the oneOf schema.
|
||||
# Undeclared properties (e.g. properties that are allowed because of the
|
||||
# additionalProperties attribute in the OAS document) are not added to
|
||||
# the dict.
|
||||
kwargs = {}
|
||||
var_names = set(oneof_class.openapi_types.keys())
|
||||
for var_name in var_names:
|
||||
if var_name in fixed_model_args:
|
||||
kwargs[var_name] = fixed_model_args[var_name]
|
||||
if not single_value_input:
|
||||
# transform js keys from input data to python keys in fixed_model_args
|
||||
fixed_model_args = change_keys_js_to_python(
|
||||
model_kwargs, oneof_class)
|
||||
|
||||
# do not try to make a model with no input args
|
||||
if len(kwargs) == 0:
|
||||
continue
|
||||
# Extract a dict with the properties that are declared in the oneOf schema.
|
||||
# Undeclared properties (e.g. properties that are allowed because of the
|
||||
# additionalProperties attribute in the OAS document) are not added to
|
||||
# the dict.
|
||||
kwargs = {}
|
||||
var_names = set(oneof_class.openapi_types.keys())
|
||||
for var_name in var_names:
|
||||
if var_name in fixed_model_args:
|
||||
kwargs[var_name] = fixed_model_args[var_name]
|
||||
|
||||
# do not try to make a model with no input args
|
||||
if len(kwargs) == 0:
|
||||
continue
|
||||
|
||||
# and use it to make the instance
|
||||
kwargs.update(constant_kwargs)
|
||||
|
||||
# and use it to make the instance
|
||||
kwargs.update(constant_args)
|
||||
try:
|
||||
oneof_instance = oneof_class(**kwargs)
|
||||
if not single_value_input:
|
||||
oneof_instance = oneof_class(**kwargs)
|
||||
else:
|
||||
if issubclass(oneof_class, ModelSimple):
|
||||
oneof_instance = oneof_class(model_arg, **constant_kwargs)
|
||||
elif oneof_class in PRIMITIVE_TYPES:
|
||||
oneof_instance = validate_and_convert_types(
|
||||
model_arg,
|
||||
(oneof_class,),
|
||||
constant_kwargs['_path_to_item'],
|
||||
constant_kwargs['_spec_property_naming'],
|
||||
constant_kwargs['_check_type'],
|
||||
configuration=constant_kwargs['_configuration']
|
||||
)
|
||||
oneof_instances.append(oneof_instance)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -1586,13 +1708,13 @@ def get_oneof_instance(self, model_args, constant_args):
|
||||
raise ApiValueError(
|
||||
"Invalid inputs given to generate an instance of %s. None "
|
||||
"of the oneOf schemas matched the input data." %
|
||||
self.__class__.__name__
|
||||
cls.__name__
|
||||
)
|
||||
elif len(oneof_instances) > 1:
|
||||
raise ApiValueError(
|
||||
"Invalid inputs given to generate an instance of %s. Multiple "
|
||||
"oneOf schemas matched the inputs, but a max of one is allowed." %
|
||||
self.__class__.__name__
|
||||
cls.__name__
|
||||
)
|
||||
return oneof_instances[0]
|
||||
|
||||
@@ -1732,7 +1854,7 @@ def validate_get_composed_info(constant_args, model_args, self):
|
||||
composed_instances = []
|
||||
allof_instances = get_allof_instances(self, model_args, constant_args)
|
||||
composed_instances.extend(allof_instances)
|
||||
oneof_instance = get_oneof_instance(self, model_args, constant_args)
|
||||
oneof_instance = get_oneof_instance(self.__class__, model_args, constant_args)
|
||||
if oneof_instance is not None:
|
||||
composed_instances.append(oneof_instance)
|
||||
anyof_instances = get_anyof_instances(self, model_args, constant_args)
|
||||
|
||||
Reference in New Issue
Block a user