From 6205ec575da91ed0aac33cea7e33b301cb68520f Mon Sep 17 00:00:00 2001 From: Anton Benkevich Date: Sun, 29 Nov 2020 09:15:13 +0000 Subject: [PATCH 1/2] Naive support for compound schema in the request body allowing to derive properties --- almdrlib/client.py | 61 +++++++++++++++++++++++++++++++++++++++++----- almdrlib/util.py | 14 +++++++++++ 2 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 almdrlib/util.py diff --git a/almdrlib/client.py b/almdrlib/client.py index bf5e84d..ddab02b 100644 --- a/almdrlib/client.py +++ b/almdrlib/client.py @@ -13,7 +13,11 @@ import json import jsonschema from jsonschema.validators import validator_for +from collections import OrderedDict +from functools import reduce + import alsdkdefs +from almdrlib.util import * from almdrlib.exceptions import AlmdrlibValueError from almdrlib.config import Config @@ -247,7 +251,7 @@ def add_content(self, content_type, schema, al_schema): if not schema: return - datatype = schema.get(OpenAPIKeyWord.TYPE) + datatype = schema.get(OpenAPIKeyWord.TYPE, derive_type_from_decomposed_schema(schema)) name = al_schema.get(OpenAPIKeyWord.NAME, OpenAPIKeyWord.DATA) if datatype == OpenAPIKeyWord.OBJECT: parameter = RequestBodyObjectParameter( @@ -786,18 +790,63 @@ def __getattr__(self, op_name): def _normalize_schema(name, schema, required=False): properties = schema.get(OpenAPIKeyWord.PROPERTIES) + oneof = schema.get(OpenAPIKeyWord.ONE_OF) + anyof = schema.get(OpenAPIKeyWord.ANY_OF) + allof = schema.get(OpenAPIKeyWord.ALL_OF) + compound_schema = oneof or anyof or allof + if properties and bool(properties): return schema - result = { - OpenAPIKeyWord.TYPE: OpenAPIKeyWord.OBJECT, - OpenAPIKeyWord.PROPERTIES: { + if compound_schema: + if oneof: + xof = OpenAPIKeyWord.ONE_OF + elif anyof: + xof = OpenAPIKeyWord.ANY_OF + elif allof: + xof = OpenAPIKeyWord.ALL_OF + + def reduce_props(acc, prop): + (k, v) = prop + existing_prop = acc.get(k, []) + if existing_prop and isinstance(existing_prop, list): + existing_prop.append(v) + acc[k] = existing_prop + return acc + elif existing_prop: + if existing_prop == v: + return acc + acc[k] = [v, existing_prop] + return acc + else: + acc[k] = v + return acc + + def add_xof(item): + (k, v) = item + if isinstance(v, list): + return k, OrderedDict({xof: v}) + else: + return item + + allprops = reduce(lambda acc, s: acc + list(s.get(OpenAPIKeyWord.PROPERTIES).items()), compound_schema, []) + reduced = reduce(reduce_props, allprops, OrderedDict()) + compound_props = OrderedDict(map(add_xof, reduced.items())) + properties = compound_props + + if not properties: + properties = OrderedDict({ name: schema - } - } + }) + result = OrderedDict({ + OpenAPIKeyWord.TYPE: derive_type_from_decomposed_schema(schema) or OpenAPIKeyWord.OBJECT, + OpenAPIKeyWord.PROPERTIES: properties + }) + if required: result[OpenAPIKeyWord.REQUIRED] = [name] + return result diff --git a/almdrlib/util.py b/almdrlib/util.py new file mode 100644 index 0000000..932fc15 --- /dev/null +++ b/almdrlib/util.py @@ -0,0 +1,14 @@ +from alsdkdefs import OpenAPIKeyWord + + +def derive_type_from_decomposed_schema(schema): + oneof = schema.get(OpenAPIKeyWord.ONE_OF) + anyof = schema.get(OpenAPIKeyWord.ANY_OF) + allof = schema.get(OpenAPIKeyWord.ALL_OF) + + if oneof or anyof or allof: + # Attempt to find type in the decomposed schema + find_type = [t.get('type') for t in allof or anyof or oneof if t.get('type')] + return find_type[0] if find_type else None + else: + return None From 155fc4f6473b91052d1fee107772edf6023bb512 Mon Sep 17 00:00:00 2001 From: Anton Benkevich Date: Mon, 30 Nov 2020 08:41:11 +0000 Subject: [PATCH 2/2] Backup, adding more tests --- almdrlib/client.py | 6 ++++-- tests/apis/testapi/testapi.v1.yaml | 29 +++++++++++++++++++++++++++++ tests/test_open_api_support.py | 13 ++++++++++++- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/almdrlib/client.py b/almdrlib/client.py index ddab02b..b5b4b39 100644 --- a/almdrlib/client.py +++ b/almdrlib/client.py @@ -799,6 +799,7 @@ def _normalize_schema(name, schema, required=False): return schema if compound_schema: + # TODO move to alertlogic-sdk-definitions normalize_node if oneof: xof = OpenAPIKeyWord.ONE_OF elif anyof: @@ -829,10 +830,11 @@ def add_xof(item): else: return item - allprops = reduce(lambda acc, s: acc + list(s.get(OpenAPIKeyWord.PROPERTIES).items()), compound_schema, []) + allprops = reduce(lambda acc, s: acc + list(s.get(OpenAPIKeyWord.PROPERTIES, OrderedDict()).items()), + compound_schema, []) reduced = reduce(reduce_props, allprops, OrderedDict()) compound_props = OrderedDict(map(add_xof, reduced.items())) - properties = compound_props + properties = compound_props if not properties: properties = OrderedDict({ diff --git a/tests/apis/testapi/testapi.v1.yaml b/tests/apis/testapi/testapi.v1.yaml index 57ad07b..0c4f596 100644 --- a/tests/apis/testapi/testapi.v1.yaml +++ b/tests/apis/testapi/testapi.v1.yaml @@ -16,6 +16,35 @@ servers: description: integration x-alertlogic-session-endpoint: true paths: + '/testapi/v1/postdata': + post: + operationId: post_data_compound + requestBody: + content: + application/json: + schema: + oneOf: + - type: object + properties: + prop: + type: string + additionalProperties: true + - type: object + properties: + prop: + type: integer + - type: string + responses: + '200': + description: 200 resp + content: + application/json: + schema: + type: object + properties: + prop: + type: string + '/testapi/v1/{account_id}/test_get_data': get: summary: Test Get Data Operation diff --git a/tests/test_open_api_support.py b/tests/test_open_api_support.py index 1810ffe..88be1ad 100644 --- a/tests/test_open_api_support.py +++ b/tests/test_open_api_support.py @@ -11,7 +11,7 @@ from almdrlib.client import Config from almdrlib.client import Operation from alsdkdefs import OpenAPIKeyWord - +from collections import OrderedDict class TestSdk_open_api_support(unittest.TestCase): """Tests for `python_boilerplate` package.""" @@ -83,3 +83,14 @@ def test_003_default_objects_creation(self): self.assertIsInstance(Session(), Session) self.assertIsInstance(Config(), Config) self.assertIsInstance(Client(self._service_name), Client) + + def test_004_test_operations_compound_schema(self): + """Test request body properties are properly normalized when schema is compound at the top level""" + client = Client(self._service_name) + operation = client.operations.get('post_data_compound') + self.assertEqual(OrderedDict([('prop', + OrderedDict([('oneOf', [ + OrderedDict([('type', 'integer')]), + OrderedDict([('type', 'string')]) + ])]))]), + operation.body._content['application/json']._properties)