From 214346dd81c1dd8c2663644bf1ac5ec8429b8dcb Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Thu, 25 Jun 2020 16:07:35 +0200 Subject: [PATCH 01/11] Zswag spec: support more http methods, x-zserio-request-part, application/x-zserio-object --- src/zswag_client/spec.py | 79 ++++++++++++++++++++++++++++++---------- 1 file changed, 59 insertions(+), 20 deletions(-) diff --git a/src/zswag_client/spec.py b/src/zswag_client/spec.py index 3c310a0..3a873db 100644 --- a/src/zswag_client/spec.py +++ b/src/zswag_client/spec.py @@ -8,30 +8,58 @@ from urllib.parse import urlparse +ZSERIO_OBJECT_CONTENT_TYPE = "application/x-zserio-request-part" +ZSERIO_REQUEST_PART = "x-zserio-request-part" +ZSERIO_REQUEST_PART_WHOLE = "*" + + class HttpMethod(Enum): GET = 0 POST = 1 + PUT = 2 + DELETE = 3 + PATCH = 4 + + +class ParamLocation(Enum): + QUERY = 0 + BODY = 1 + PATH = 2 class ParamFormat(Enum): - QUERY_PARAM_BASE64 = 0 - BODY_BINARY = 1 + STRING = 0 + BYTE = 1 + HEX = 2 + BINARY = 3 + + +class ParamSpec: + + def __init__(self, *, name: str = "", format: ParamFormat, location: ParamLocation, zserio_request_part: str): + # https://github.com/Klebert-Engineering/zswag/issues/15 + assert location in (ParamLocation.QUERY, ParamLocation.BODY) + # https://github.com/Klebert-Engineering/zswag/issues/19 + assert zserio_request_part == ZSERIO_REQUEST_PART_WHOLE + # https://github.com/Klebert-Engineering/zswag/issues/20 + assert \ + (format == ParamFormat.BINARY and location == ParamLocation.BODY) or \ + (format == ParamFormat.BYTE and location == ParamLocation.QUERY) + self.name = name + self.format = format + self.location = location + self.zserio_request_part = zserio_request_part class MethodSpec: - def __init__(self, name: str, path: str, http_method: HttpMethod, param_format: ParamFormat, param_name: str): + def __init__(self, name: str, path: str, http_method: HttpMethod, params: List[ParamSpec]): + # https://github.com/Klebert-Engineering/zswag/issues/19 + assert len(params) == 1 self.name = name self.path = path self.http_method = http_method - self.param_format = param_format - self.param_name = param_name - assert \ - self.http_method == HttpMethod.GET and param_format == ParamFormat.QUERY_PARAM_BASE64 or \ - self.http_method == HttpMethod.POST - assert \ - self.param_format == ParamFormat.QUERY_PARAM_BASE64 and param_name or \ - self.param_format == ParamFormat.BODY_BINARY + self.params = params class ZserioSwaggerSpec: @@ -49,19 +77,30 @@ def __init__(self, spec_url_or_path: str): for method, method_spec in path_spec.items(): http_method = HttpMethod[method.upper()] name = method_spec["operationId"] - param_format = ParamFormat.BODY_BINARY - expected_param_name = "requestData" - if "parameters" in method_spec and \ - any(param for param in method_spec["parameters"] if param["name"] == expected_param_name): - param_format = ParamFormat.QUERY_PARAM_BASE64 - else: - expected_param_name = "" + params: List[ParamSpec] = [] + if "requestBody" in method_spec: + assert "content" in method_spec["requestBody"] + for content_type in method_spec["requestBody"]["content"]: + if content_type == ZSERIO_OBJECT_CONTENT_TYPE: + params.append(ParamSpec( + format=ParamFormat.BINARY, + location=ParamLocation.BODY, + zserio_request_part=ZSERIO_REQUEST_PART_WHOLE)) + if "parameters" in method_spec: + for param in method_spec["parameters"]: + if ZSERIO_REQUEST_PART not in param: + continue + assert "schema" in param and "format" in param["schema"] + params.append(ParamSpec( + name=param["name"], + format=ParamFormat[param["schema"]["format"].upper()], + location=ParamLocation[param["in"].upper()], + zserio_request_part=param[ZSERIO_REQUEST_PART])) method_spec_object = MethodSpec( name=name, path=path, http_method=http_method, - param_format=param_format, - param_name=expected_param_name) + params=params) assert name not in self.methods self.methods[name] = method_spec_object From 9094b21e5d2aa4ebad47fb7bf3e465c5b2d69f12 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Thu, 25 Jun 2020 16:07:59 +0200 Subject: [PATCH 02/11] Zswag client: support more http methods, x-zserio-request-part, application/x-zserio-object --- src/zswag_client/client.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/zswag_client/client.py b/src/zswag_client/client.py index 05e70bb..520ab58 100644 --- a/src/zswag_client/client.py +++ b/src/zswag_client/client.py @@ -2,7 +2,7 @@ import zserio import base64 -from .spec import ZserioSwaggerSpec, HttpMethod, ParamFormat +from .spec import ZserioSwaggerSpec, HttpMethod, ParamFormat, ParamLocation, ParamSpec from urllib.parse import urlparse import os @@ -70,14 +70,23 @@ def callMethod(self, method_name, request_data, context=None): try: method_spec = self.spec.method_spec(method_name) kwargs = {} - if method_spec.param_format == ParamFormat.QUERY_PARAM_BASE64: - kwargs["params"] = {"requestData": base64.urlsafe_b64encode(request_data)} - else: - kwargs["data"] = request_data + for param in method_spec.params: + if param.location == ParamLocation.QUERY: + kwargs["params"] = {"requestData": base64.urlsafe_b64encode(request_data)} + elif param.location == ParamLocation.BODY: + kwargs["data"] = request_data if method_spec.http_method == HttpMethod.GET: response = requests.get(self.path + method_name, **kwargs) - else: + elif method_spec.http_method == HttpMethod.POST: response = requests.post(self.path + method_name, **kwargs) + elif method_spec.http_method == HttpMethod.DELETE: + response = requests.delete(self.path + method_name, **kwargs) + elif method_spec.http_method == HttpMethod.PUT: + response = requests.put(self.path + method_name, **kwargs) + elif method_spec.http_method == HttpMethod.PATCH: + response = requests.patch(self.path + method_name, **kwargs) + else: + raise zserio.ServiceException("Unsupported HTTP method!") if response.status_code != requests.codes.ok: raise zserio.ServiceException(str(response.status_code)) return response.content From fc4dc47df7e6f413355918095014d46e5b03e5bd Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Thu, 25 Jun 2020 16:08:26 +0200 Subject: [PATCH 03/11] Zswag app: support x-zserio-request-part, application/x-zserio-object --- src/zswag/app.py | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/zswag/app.py b/src/zswag/app.py index ea874e6..51d01e7 100644 --- a/src/zswag/app.py +++ b/src/zswag/app.py @@ -9,7 +9,7 @@ from typing import Type from .doc import get_doc_str, IdentType, md_filter_definition -from zswag_client.spec import ZserioSwaggerSpec, ParamFormat +from zswag_client.spec import ZserioSwaggerSpec, ParamFormat, ParamLocation, ZSERIO_REQUEST_PART, ZSERIO_OBJECT_CONTENT_TYPE, ZSERIO_REQUEST_PART_WHOLE # Name of variable that is added to controller @@ -125,13 +125,17 @@ def myApi(request): continue method_spec = self.spec.method_spec(method_name) - if method_spec.param_format == ParamFormat.QUERY_PARAM_BASE64: - def wsgi_method(request_data, fun=zserio_modem_function): - request_data = base64.urlsafe_b64decode(request_data) - return bytes(fun(request_data, None)) - else: - def wsgi_method(body, fun=zserio_modem_function): - return bytes(fun(body, None)) + param_spec = method_spec.params[0] + + def wsgi_method(fun=zserio_modem_function, param=param_spec, **kwargs): + param_name = param.name if param.location != ParamLocation.BODY else "body" + assert param_name in kwargs + param_value = kwargs[param_name] + if param.format == ParamFormat.BYTE: + param_value = base64.urlsafe_b64decode(param_value) + else: + assert param.format == ParamFormat.BINARY + return bytes(fun(param_value, None)) setattr(self.service_instance, method_name, wsgi_method) def method_impl(request, ctx=None, fun=user_function): @@ -177,21 +181,19 @@ def generate_openapi_schema(self): "servers": [], "paths": { f"/{method_info.name}": { - "get": { + "post": { "summary": method_info.docstring, "description": method_info.docstring, "operationId": method_info.name, - "parameters": [{ - "name": "requestData", - "in": "query", - "description": method_info.argdoc, - "required": True, - "schema": { - "type": "string", - "default": "Base64-encoded bytes", - "format": "byte" + "requestBody": { + "content": { + ZSERIO_OBJECT_CONTENT_TYPE: { + "schema": { + "type": "string" + } + } } - }], + }, "responses": { "200": { "description": method_info.returndoc, From 3cd9f244cf453490a2bba67c0763ae0dbca92bfa Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Thu, 25 Jun 2020 16:08:41 +0200 Subject: [PATCH 04/11] Bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c2e8a60..d624f52 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ setuptools.setup( name="zswag", - version="0.5.0", + version="0.6.0-pre", url="https://github.com/klebert-engineering/zswag", author="Klebert Engineering", author_email="j.birkner@klebert-engineering.de", From 01037e8d97de28de1287b5f32763857cf9e16c90 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Thu, 25 Jun 2020 16:15:43 +0200 Subject: [PATCH 05/11] Zswag README: support x-zserio-request-part, application/x-zserio-object --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 83edf4e..31be0e0 100644 --- a/README.md +++ b/README.md @@ -114,15 +114,14 @@ paths: #### Option: Base64 URL Parameter Format To use the __Base64 URL parameter format__, use the snippet below in you method spec. -Zswag will use this format if a parameter named `requestData` exists. ```yaml parameters: - description: '' in: query name: requestData required: true + x-zserio-request-part: "*" # The parameter represents the whole zserio request object schema: - default: Base64-encoded bytes format: byte type: string ``` @@ -133,11 +132,10 @@ To use the Binary Body Parameter Format, use the snippet below in your method sp ```yaml requestBody: content: - application/x-binary: + application/x-zserio-object: schema: type: string ``` -Note: Binary parameter passing is only allowed with `POST` http requests. #### Option: Server URL Base Path From 105e519afae02435f2ad253bcc4bc4fa63684199 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Thu, 25 Jun 2020 16:16:07 +0200 Subject: [PATCH 06/11] Do not enforce python parameter name conversion. --- src/zswag/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zswag/app.py b/src/zswag/app.py index 51d01e7..363d9b1 100644 --- a/src/zswag/app.py +++ b/src/zswag/app.py @@ -151,7 +151,7 @@ def method_impl(request, ctx=None, fun=user_function): self.add_api( yaml_basename, arguments={"title": f"REST API for {service_type.__name__}"}, - pythonic_params=True) + pythonic_params=False) def verify_openapi_schema(self): for method_name in self.service_instance._methodMap: From b1a8b4722a6212adbf5afd8a9d3bc25a080192ca Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Thu, 25 Jun 2020 16:33:42 +0200 Subject: [PATCH 07/11] Add calc example test instructions to README. --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 31be0e0..ebb2d5c 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,14 @@ Alternatively, clone this repository, and run pip3 install -e . ``` +## Running the calc test example + +```bash +PYTHONPATH=$PWD/test +python3 -m calc server & +python3 -m calc client +``` + ## Creating a Swagger service from zserio `ZserioSwaggerApp` gives you the power to marry a user-written app controller @@ -102,12 +110,12 @@ OpenAPI YAML file: #### Option: HTTP method -To change the **HTTP method**, simply place either `get` or `post` +To change the **HTTP method**, simply place the desired method as the key under the method path, such as in the following example: ```yaml paths: /methodName: - {get|post}: + {get|post|put|patch|delete}: ... ``` From 7bd8ec4f6646d483d5cc0bf8dcbaee238bdbc101 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Thu, 25 Jun 2020 16:45:23 +0200 Subject: [PATCH 08/11] Fix x-zserio-object. --- README.md | 2 +- setup.py | 2 +- src/zswag_client/spec.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ebb2d5c..3486e3f 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Alternatively, clone this repository, and run pip3 install -e . ``` -## Running the calc test example +## Running the remote calculator test example ```bash PYTHONPATH=$PWD/test diff --git a/setup.py b/setup.py index d624f52..2beffa2 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ setuptools.setup( name="zswag", - version="0.6.0-pre", + version="0.6.0rc1", url="https://github.com/klebert-engineering/zswag", author="Klebert Engineering", author_email="j.birkner@klebert-engineering.de", diff --git a/src/zswag_client/spec.py b/src/zswag_client/spec.py index 3a873db..c660743 100644 --- a/src/zswag_client/spec.py +++ b/src/zswag_client/spec.py @@ -8,7 +8,7 @@ from urllib.parse import urlparse -ZSERIO_OBJECT_CONTENT_TYPE = "application/x-zserio-request-part" +ZSERIO_OBJECT_CONTENT_TYPE = "application/x-zserio-object" ZSERIO_REQUEST_PART = "x-zserio-request-part" ZSERIO_REQUEST_PART_WHOLE = "*" From f83328ec9818439680d2a274117a598eeed7931c Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Fri, 3 Jul 2020 14:04:01 +0200 Subject: [PATCH 09/11] Use standard base64 en-/decoding for format "byte" --- src/zswag/app.py | 2 +- src/zswag_client/client.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/zswag/app.py b/src/zswag/app.py index 363d9b1..0bd1050 100644 --- a/src/zswag/app.py +++ b/src/zswag/app.py @@ -132,7 +132,7 @@ def wsgi_method(fun=zserio_modem_function, param=param_spec, **kwargs): assert param_name in kwargs param_value = kwargs[param_name] if param.format == ParamFormat.BYTE: - param_value = base64.urlsafe_b64decode(param_value) + param_value = base64.b64decode(param_value) else: assert param.format == ParamFormat.BINARY return bytes(fun(param_value, None)) diff --git a/src/zswag_client/client.py b/src/zswag_client/client.py index 520ab58..d1fb25a 100644 --- a/src/zswag_client/client.py +++ b/src/zswag_client/client.py @@ -72,7 +72,7 @@ def callMethod(self, method_name, request_data, context=None): kwargs = {} for param in method_spec.params: if param.location == ParamLocation.QUERY: - kwargs["params"] = {"requestData": base64.urlsafe_b64encode(request_data)} + kwargs["params"] = {"requestData": base64.b64encode(request_data)} elif param.location == ParamLocation.BODY: kwargs["data"] = request_data if method_spec.http_method == HttpMethod.GET: From 5d5e57c43ec7840ba014fe76ef455db8a2ac11e4 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Fri, 3 Jul 2020 14:33:42 +0200 Subject: [PATCH 10/11] Set argdoc as "requestBody" description string --- src/zswag/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/zswag/app.py b/src/zswag/app.py index 0bd1050..aa40b9e 100644 --- a/src/zswag/app.py +++ b/src/zswag/app.py @@ -186,6 +186,7 @@ def generate_openapi_schema(self): "description": method_info.docstring, "operationId": method_info.name, "requestBody": { + "description": method_info.argdoc, "content": { ZSERIO_OBJECT_CONTENT_TYPE: { "schema": { From 2bb1d848c221a03a6907e297ddcdd475e757fbf7 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Fri, 3 Jul 2020 14:49:25 +0200 Subject: [PATCH 11/11] Version is 0.6.0. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2beffa2..965aac7 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ setuptools.setup( name="zswag", - version="0.6.0rc1", + version="0.6.0", url="https://github.com/klebert-engineering/zswag", author="Klebert Engineering", author_email="j.birkner@klebert-engineering.de",