diff --git a/packages/autorest.python/ChangeLog.md b/packages/autorest.python/ChangeLog.md index fe78733b3e2..4c1b76d6e27 100644 --- a/packages/autorest.python/ChangeLog.md +++ b/packages/autorest.python/ChangeLog.md @@ -1,5 +1,21 @@ # Release History +### 2023-01-xx - 6.xx.xx + +| Library | Min Version | +| ----------------------------------------------------------------------- | ----------- | +| `@autorest/core` | `3.9.2` | +| `@autorest/modelerfour` | `4.24.3` | +| `azure-core` dep of generated code | `1.24.0` | +| `isodate` dep of generated code | `0.6.1` | +| `msrest` dep of generated code (If generating legacy code) | `0.7.1` | +| `azure-mgmt-core` dep of generated code (If generating mgmt plane code) | `1.3.2` | +| `typing-extensions` dep of generated code (If generating with constants) | `4.0.1` | + +**New Features** + +- Flatten JSONModelType body properties as operation parameters #1623 + ### 2022-12-16 - 6.2.13 | Library | Min Version | diff --git a/packages/autorest.python/autorest/codegen/models/parameter.py b/packages/autorest.python/autorest/codegen/models/parameter.py index 268fe5bf11c..9ce7f4f4e10 100644 --- a/packages/autorest.python/autorest/codegen/models/parameter.py +++ b/packages/autorest.python/autorest/codegen/models/parameter.py @@ -22,6 +22,8 @@ from .base import BaseType from .constant_type import ConstantType from .utils import add_to_description +from .combined_type import CombinedType +from .model_type import JSONModelType if TYPE_CHECKING: from .code_model import CodeModel @@ -241,6 +243,20 @@ def content_types(self) -> List[str]: def default_content_type(self) -> str: return self.yaml_data["defaultContentType"] + @staticmethod + def _has_json_model_type(t: BaseType) -> bool: + if isinstance(t, JSONModelType): + return True + if isinstance(t, CombinedType): + for sub_t in t.types: + if BodyParameter._has_json_model_type(sub_t): + return True + return False + + @property + def has_json_model_type(self) -> bool: + return BodyParameter._has_json_model_type(self.type) + @classmethod def from_yaml( cls, yaml_data: Dict[str, Any], code_model: "CodeModel" @@ -314,6 +330,7 @@ def __init__( self.in_overload: bool = self.yaml_data["inOverload"] self.in_overriden: bool = self.yaml_data.get("inOverriden", False) self.delimiter: Optional[ParameterDelimeter] = self.yaml_data.get("delimiter") + self.in_flattened_body: bool = self.yaml_data.get("inFlattenedBody", False) @property def in_method_signature(self) -> bool: @@ -334,9 +351,13 @@ def is_content_type(self) -> bool: return bool(self.rest_api_name) and self.rest_api_name.lower() == "content-type" @property - def method_location(self) -> ParameterMethodLocation: + def method_location( # pylint: disable=too-many-return-statements + self, + ) -> ParameterMethodLocation: if not self.in_method_signature: raise ValueError(f"Parameter '{self.client_name}' is not in the method.") + if self.code_model.options["models_mode"] == "dpg" and self.in_flattened_body: + return ParameterMethodLocation.KEYWORD_ONLY if self.grouper: return ParameterMethodLocation.POSITIONAL if self.constant: diff --git a/packages/autorest.python/autorest/codegen/serializers/builder_serializer.py b/packages/autorest.python/autorest/codegen/serializers/builder_serializer.py index 7e71b56cf9a..359f3e50655 100644 --- a/packages/autorest.python/autorest/codegen/serializers/builder_serializer.py +++ b/packages/autorest.python/autorest/codegen/serializers/builder_serializer.py @@ -31,6 +31,8 @@ MultipartBodyParameter, Property, RequestBuilderType, + JSONModelType, + CombinedType, ) from .parameter_serializer import ParameterSerializer, PopKwargType from . import utils @@ -146,6 +148,25 @@ def _serialize_flattened_body(body_parameter: BodyParameter) -> List[str]: return retval +def _serialize_json_model_body(body_parameter: BodyParameter) -> List[str]: + retval: List[str] = [] + if not body_parameter.property_to_parameter_name: + raise ValueError( + "This method can't be called if the operation doesn't need parameter flattening" + ) + + retval.append(f"if {body_parameter.client_name} is None:") + parameter_string = ", \n".join( + f'"{property_name}": {parameter_name}' + for property_name, parameter_name in body_parameter.property_to_parameter_name.items() + ) + model_type = cast(ModelType, body_parameter.type) + if isinstance(model_type, CombinedType): + model_type = next(t for t in model_type.types if isinstance(t, JSONModelType)) + retval.append(f" {body_parameter.client_name} = {{{parameter_string}}}") + return retval + + def _serialize_multipart_body(builder: BuilderType) -> List[str]: retval: List[str] = [] body_param = cast(MultipartBodyParameter, builder.parameters.body_parameter) @@ -928,6 +949,12 @@ def _call_request_builder_helper( # pylint: disable=too-many-statements if builder.parameters.has_body and builder.parameters.body_parameter.flattened: # unflatten before passing to request builder as well retval.extend(_serialize_flattened_body(builder.parameters.body_parameter)) + if ( + builder.parameters.has_body + and builder.parameters.body_parameter.has_json_model_type + and any(p.in_flattened_body for p in builder.parameters.parameters) + ): + retval.extend(_serialize_json_model_body(builder.parameters.body_parameter)) if builder.overloads: # we are only dealing with two overloads. If there are three, we generate an abstract operation retval.extend(self._initialize_overloads(builder, is_paging=is_paging)) diff --git a/packages/autorest.python/autorest/preprocess/__init__.py b/packages/autorest.python/autorest/preprocess/__init__.py index dafce3a9c74..c96a5c2bd82 100644 --- a/packages/autorest.python/autorest/preprocess/__init__.py +++ b/packages/autorest.python/autorest/preprocess/__init__.py @@ -60,13 +60,21 @@ def update_overload_section( overload_h["type"] = original_h["type"] -def add_overload(yaml_data: Dict[str, Any], body_type: Dict[str, Any]): +def add_overload( + yaml_data: Dict[str, Any], body_type: Dict[str, Any], for_flatten_params=False +): overload = copy.deepcopy(yaml_data) overload["isOverload"] = True overload["bodyParameter"]["type"] = body_type overload["overloads"] = [] + if for_flatten_params: + overload["bodyParameter"]["flattened"] = True + else: + overload["parameters"] = [ + p for p in overload["parameters"] if not p.get("inFlattenedBody") + ] # for yaml sync, we need to make sure all of the responses, parameters, and exceptions' types have the same yaml id for overload_p, original_p in zip(overload["parameters"], yaml_data["parameters"]): overload_p["type"] = original_p["type"] @@ -107,6 +115,10 @@ def add_overloads_for_body_param(yaml_data: Dict[str, Any]) -> None: ): continue yaml_data["overloads"].append(add_overload(yaml_data, body_type)) + if body_type.get("type") == "model" and body_type.get("base") == "json": + yaml_data["overloads"].append( + add_overload(yaml_data, body_type, for_flatten_params=True) + ) content_type_param = next( p for p in yaml_data["parameters"] if p["restApiName"].lower() == "content-type" ) @@ -162,9 +174,9 @@ def update_parameter(yaml_data: Dict[str, Any]) -> None: if yaml_data.get("propertyToParameterName"): # need to create a new one with padded keys and values yaml_data["propertyToParameterName"] = { - pad_reserved_words(prop, PadType.PROPERTY) - .lower(): pad_reserved_words(param_name, PadType.PARAMETER) - .lower() + pad_reserved_words(prop, PadType.PROPERTY): pad_reserved_words( + param_name, PadType.PARAMETER + ) for prop, param_name in yaml_data["propertyToParameterName"].items() } diff --git a/packages/cadl-python/CHANGELOG.md b/packages/cadl-python/CHANGELOG.md index 636a9d3642d..0cc1265a2e7 100644 --- a/packages/cadl-python/CHANGELOG.md +++ b/packages/cadl-python/CHANGELOG.md @@ -5,6 +5,7 @@ **Other Changes** - Support multiple authentication #1626 +- Flatten JSONModelType body properties as operation parameters #1623 **Bug Fixes** diff --git a/packages/cadl-python/src/emitter.ts b/packages/cadl-python/src/emitter.ts index 89dd96b1d1b..52f5bc6b6fd 100644 --- a/packages/cadl-python/src/emitter.ts +++ b/packages/cadl-python/src/emitter.ts @@ -411,6 +411,30 @@ function emitContentTypeParameter( }; } +function emitFlattenedParameter( + bodyParameter: Record, + property: Record, +): Record { + return { + checkClientInput: false, + clientDefaultValue: null, + clientName: property.clientName, + delimiter: null, + description: property.description, + implementation: "Method", + inDocstring: true, + inFlattenedBody: true, + inOverload: false, + inOverriden: false, + isApiVersion: bodyParameter["isApiVersion"], + location: "other", + optional: property["optional"], + restApiName: null, + skipUrlEncoding: false, + type: property["type"], + }; +} + function getConstantType(key: string): Record { const cache = simpleTypesMap.get(key); if (cache) { @@ -614,6 +638,13 @@ function emitBasicOperation(program: Program, operation: Operation, operationGro if (parameters.filter((e) => e.restApiName.toLowerCase() === "content-type").length === 0) { parameters.push(emitContentTypeParameter(bodyParameter, isOverload, isOverriden)); } + if (bodyParameter.type.type == "model" && bodyParameter.type.base == "json") { + bodyParameter["propertyToParameterName"] = {}; + for (const property of bodyParameter.type.properties) { + bodyParameter["propertyToParameterName"][property["restApiName"]] = property["clientName"]; + parameters.push(emitFlattenedParameter(bodyParameter, property)); + } + } } const name = camelToSnakeCase(operation.name); return {