Skip to content

Commit

Permalink
Merge pull request #48 from Klebert-Engineering/feature/use-zserio-2.…
Browse files Browse the repository at this point in the history
…3.0-final

Use final zserio 2.3.0
  • Loading branch information
josephbirkner authored Apr 13, 2021
2 parents 7f61aa6 + d65ad02 commit 0629b10
Show file tree
Hide file tree
Showing 11 changed files with 302 additions and 58 deletions.
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ enable_testing()

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(ZSWAG_VERSION 0.7.0rc1)
set(ZSWAG_VERSION 1.0.0)

if (NOT MSVC)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
Expand Down
28 changes: 12 additions & 16 deletions libs/pyzswagcl/py-openapi-client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,9 @@ namespace
void PyOpenApiClient::bind(py::module_& m) {
auto serviceClient = py::class_<PyOpenApiClient>(m, "OAClient")
.def(py::init<std::string, bool, Headers>(), "url"_a, "is_local_file"_a = false, "headers"_a = Headers())
// zserio <= 2.2.0
.def("callMethod", &PyOpenApiClient::callMethod,
"methodName"_a, "requestData"_a, "context"_a)
// zserio >= 2.3.0
.def("call_method", &PyOpenApiClient::callMethod,
"method_name"_a, "request_data"_a, "context"_a);
"method_name"_a, "request"_a, "unused"_a);

py::object serviceClientBase = py::module::import("zserio").attr("ServiceInterface");
serviceClient.attr("__bases__") = py::make_tuple(serviceClientBase) + serviceClient.attr("__bases__");
Expand All @@ -92,18 +89,17 @@ PyOpenApiClient::PyOpenApiClient(std::string const& openApiUrl,

std::vector<uint8_t> PyOpenApiClient::callMethod(
const std::string& methodName,
py::bytearray& requestData,
py::handle context)
py::object request,
py::object unused)
{
if (!context) {
throw std::runtime_error(stx::format(
"Unset context argument for call to {}! Please pass the request also as context.",
methodName));
if (!request) {
throw std::runtime_error("The request argument is None!");
}

auto response = client_->call(methodName, [&](const std::string& parameter, const std::string& field, ParameterValueHelper& helper)
{
if (field == ZSERIO_REQUEST_PART_WHOLE) {
auto requestData = request.attr("byte_array");
py::buffer_info info(py::buffer(requestData).request());
auto* data = reinterpret_cast<uint8_t*>(info.ptr);
auto length = static_cast<size_t>(info.size);
Expand All @@ -112,25 +108,25 @@ std::vector<uint8_t> PyOpenApiClient::callMethod(

auto parts = stx::split<std::vector<std::string>>(field, ".");
auto currentField = parts.begin();
auto value = context.ptr();
auto value = request.attr("zserio_object").cast<py::object>();

while (currentField != parts.end()) {
auto internalFieldName = stx::format("_{}_", *currentField);
if (!PyObject_HasAttrString(value, internalFieldName.c_str())) {
if (!py::hasattr(value, internalFieldName.c_str())) {
throw std::runtime_error(stx::format("Could not find request field {} in method {}.",
stx::join(parts.begin(), currentField + 1, "."),
methodName));
}
value = PyObject_GetAttrString(value, internalFieldName.c_str());
value = value.attr(internalFieldName.c_str());
assert(value);
++currentField;
}

if (PySequence_Check(value)) {
return helper.array(valuesFromPyArray(value));
if (PySequence_Check(value.ptr())) {
return helper.array(valuesFromPyArray(value.ptr()));
}

return helper.value(valueFromPyObject(value));
return helper.value(valueFromPyObject(value.ptr()));
});

std::vector<uint8_t> responseData;
Expand Down
4 changes: 2 additions & 2 deletions libs/pyzswagcl/py-openapi-client.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ class PyOpenApiClient

std::vector<uint8_t> callMethod(
const std::string& methodName,
py::bytearray& requestData,
py::handle context);
py::object request,
py::object unused);

private:
std::unique_ptr<zswagcl::OpenAPIClient> client_;
Expand Down
6 changes: 3 additions & 3 deletions libs/zswag/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def my_api(request):
self.verify_openapi_schema()

# Re-route service impl methods
for method_name in self.service_instance.METHOD_NAMES:
for method_name in self.service_instance.method_names:
method_snake_name = to_snake(method_name)
user_function = getattr(self.controller, method_snake_name)
zserio_modem_function = getattr(self.service_instance, f"_{method_snake_name}_method")
Expand All @@ -157,7 +157,7 @@ def wsgi_method(fun=zserio_modem_function, spec=method_spec, req_t=request_type,
request_blob = kwargs["body"]
else:
request_blob = request_object_blob(req_t=req_t, spec=spec, **kwargs)
return bytes(fun(request_blob, None))
return bytes(fun(request_blob, None).byte_array)
setattr(self.service_instance, method_name, wsgi_method)

def method_impl(request, ctx=None, fun=user_function):
Expand All @@ -184,7 +184,7 @@ def method_impl(request, ctx=None, fun=user_function):
pythonic_params=False)

def verify_openapi_schema(self):
for method_name in self.service_instance.METHOD_NAMES:
for method_name in self.service_instance.method_names:
if method_name not in self.spec:
raise IncompleteSchemaError(self.yaml_path, method_name)

10 changes: 5 additions & 5 deletions libs/zswag/gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,16 +126,16 @@ def config_for_method(self, method_name: str) -> Tuple[str, OAParamLocation, boo
return self.config["*"]

def generate(self):
service_name_parts = self.service_instance.SERVICE_FULL_NAME.split(".")
service_name_parts = self.service_instance.service_full_name.split(".")
schema = {
"openapi": "3.0.0",
"info": {
"title": ".".join(service_name_parts[1:]),
"description": md_filter_definition(get_doc_str(
ident_type=IdentType.SERVICE,
pkg_path=self.zs_pkg_path,
ident=self.service_instance.SERVICE_FULL_NAME,
fallback=[f"REST API for {self.service_instance.SERVICE_FULL_NAME}"]
ident=self.service_instance.service_full_name,
fallback=[f"REST API for {self.service_instance.service_full_name}"]
)[0]),
"contact": {
"email": "[email protected]"
Expand Down Expand Up @@ -169,7 +169,7 @@ def generate(self):
},
} for method_info in (
self.generate_method_info(method_name)
for method_name in self.service_instance.METHOD_NAMES)
for method_name in self.service_instance.method_names)
}
}
yaml.dump(schema, self.output, default_flow_style=False)
Expand All @@ -182,7 +182,7 @@ def generate_method_info(self, method_name: str) -> MethodSchemaInfo:
doc_strings = get_doc_str(
ident_type=IdentType.RPC,
pkg_path=self.zs_pkg_path,
ident=f"{self.service_instance.SERVICE_FULL_NAME}.{method_name}")
ident=f"{self.service_instance.service_full_name}.{method_name}")
if doc_strings:
result.docstring = doc_strings[0]
result.returntype = doc_strings[1]
Expand Down
22 changes: 12 additions & 10 deletions libs/zswag/reflect.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import zserio
import struct
import functools
from typing import Type, Tuple, Any, Dict, Union, Optional, get_args, get_origin, List
import importlib
from typing import Type, Tuple, Any, Dict, Union, Optional, get_args, get_origin, List, get_type_hints
from pyzswagcl import OAMethod, OAParam, OAParamFormat, ZSERIO_REQUEST_PART_WHOLE
from re import compile as re

Expand All @@ -30,7 +31,7 @@ def __init__(self, member_name: str, member_type: str, method_name: str = ""):


# Table here: https://docs.python.org/3/library/struct.html#format-characters
C_STRUCT_LITERAL_PER_TYPE_AND_SIZE: Dict[Tuple[type, int], str] = {
C_STRUCT_LITERAL_PER_TYPE_AND_SIZE: Dict[Tuple[Any, int], str] = {
(bool, 1): "!b",
(bool, 2): "!h",
(bool, 4): "!i",
Expand Down Expand Up @@ -66,7 +67,7 @@ def _getattr(obj, attr):
# Get the request type for a zserio service method.
def service_method_request_type(service_instance: Any, method_name: str) -> Type:
zserio_impl_function = getattr(service_instance, f"_{to_snake(method_name)}_impl")
result = zserio_impl_function.__func__.__annotations__["request"]
result = get_type_hints(zserio_impl_function)["request"]
assert inspect.isclass(result)
return result

Expand All @@ -82,13 +83,18 @@ def to_snake(s: str, patterns=(re("([a-z])([A-Z])"), re("([0-9A-Z])([A-Z][a-z])"
# We determine whether Nested is a zserio struct type or a scalar builtin
# type (int, float etc.) and return either (None, scalar_type) or
# (zserio_struct_type, None). Otherwise return (None, None).
def unpack_zserio_arg_type(t: Type) -> Tuple[Optional[Type], Optional[Type]]:
def unpack_zserio_arg_type(t: Type, debug_field_name: str) -> Tuple[Optional[Type], Optional[Type]]:
if get_origin(t) is Union:
union_args = get_args(t)
if len(union_args) == 2 and union_args[1] is NONE_T:
result = union_args[0]
if result in SCALAR_T:
return None, result
if get_origin(result) is list:
arg_type = [get_args(result)[0]]
if arg_type[0] not in SCALAR_T:
raise UnsupportedArrayParameterError(debug_field_name, arg_type[0].__name__)
return None, arg_type
if inspect.isclass(result):
return result, None
return None, None
Expand All @@ -106,10 +112,10 @@ def make_instance_and_typeinfo(t: Type, field_name_prefix="") -> Tuple[Any, Dict
# params: Nested object init params are typed as Union[Nested, None]. We then
# instantiate the child object's type and assign it to the parent member.
if hasattr(result_instance.__init__, "__func__"):
for arg_name, arg_type in result_instance.__init__.__func__.__annotations__.items():
for arg_name, arg_type in get_type_hints(result_instance.__init__).items():
if arg_name.endswith("_"):
field_name: str = arg_name.strip('_')
compound_type, scalar_type = unpack_zserio_arg_type(arg_type)
compound_type, scalar_type = unpack_zserio_arg_type(arg_type, field_name_prefix+field_name)
if compound_type:
instance, member_types = make_instance_and_typeinfo(
compound_type, field_name_prefix+field_name+".")
Expand All @@ -118,10 +124,6 @@ def make_instance_and_typeinfo(t: Type, field_name_prefix="") -> Tuple[Any, Dict
continue
elif scalar_type:
arg_type = scalar_type
elif get_origin(arg_type) is list:
arg_type = [get_args(arg_type)[0]]
if arg_type[0] not in SCALAR_T:
raise UnsupportedArrayParameterError(field_name_prefix+field_name, arg_type[0].__name__)
result_member_types[field_name_prefix+field_name] = arg_type
return result_instance, result_member_types

Expand Down
20 changes: 10 additions & 10 deletions libs/zswag/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@ target_link_libraries(${PROJECT_NAME}
# ourselves some build minutes and workflow headache.
if (ZSWAG_ENABLE_TESTING AND NOT APPLE)
add_test(
NAME
zswag-server-integration
WORKING_DIRECTORY
"${CMAKE_CURRENT_SOURCE_DIR}"
COMMAND
bash "test_integration.bash"
-w "${WHEEL_DEPLOY_DIRECTORY}"
-b "python -m zswag.test.calc server 127.0.0.1:16161"
-f "$<TARGET_FILE:${PROJECT_NAME}> http://127.0.0.1:16161/openapi.json"
-f "python -m zswag.test.calc client 127.0.0.1:16161")
NAME
zswag-server-integration
WORKING_DIRECTORY
"${CMAKE_CURRENT_SOURCE_DIR}"
COMMAND
bash "test_integration.bash"
-w "${WHEEL_DEPLOY_DIRECTORY}"
-b "python -m zswag.test.calc server 127.0.0.1:16161"
-f "$<TARGET_FILE:${PROJECT_NAME}> http://127.0.0.1:16161/openapi.json"
-f "python -m zswag.test.calc client 127.0.0.1:16161")

add_test(
NAME
Expand Down
20 changes: 10 additions & 10 deletions libs/zswag/test/calc/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def run_test(aspect, request, fn, expect):
print(f"[py-test-client] -> Instantiating client.", flush=True)
client = api.Calculator.Client(OAClient(f"http://{host}:{port}/openapi.json"))
print(f"[py-test-client] -> Running request.", flush=True)
resp = fn(client, request, request)
resp = fn(client, request)
if resp.value == expect:
print(f"[py-test-client] -> Success.", flush=True)
else:
Expand All @@ -30,55 +30,55 @@ def run_test(aspect, request, fn, expect):
run_test(
"Pass fields in path and query",
api.BaseAndExponent(api.I32(2), api.I32(3)),
api.Calculator.Client.power_method,
api.Calculator.Client.power,
8.)

run_test(
"Pass hex-encoded array in query",
api.Integers([100, -200, 400]),
api.Calculator.Client.int_sum_method,
api.Calculator.Client.int_sum,
300.)

run_test(
"Pass base64url-encoded byte array in path",
api.Bytes([8, 16, 32, 64]),
api.Calculator.Client.byte_sum_method,
api.Calculator.Client.byte_sum,
120.)

run_test(
"Pass base64-encoded long array in path",
api.Integers([1, 2, 3, 4]),
api.Calculator.Client.int_mul_method,
api.Calculator.Client.int_mul,
24.)

run_test(
"Pass float array in query.",
api.Doubles([34.5, 2.]),
api.Calculator.Client.float_mul_method,
api.Calculator.Client.float_mul,
69.)

run_test(
"Pass bool array in query (expect false).",
api.Bools([True, False]),
api.Calculator.Client.bit_mul_method,
api.Calculator.Client.bit_mul,
False)

run_test(
"Pass bool array in query (expect true).",
api.Bools([True, True]),
api.Calculator.Client.bit_mul_method,
api.Calculator.Client.bit_mul,
True)

run_test(
"Pass request as blob in body",
api.Double(1.),
api.Calculator.Client.identity_method,
api.Calculator.Client.identity,
1.)

run_test(
"Pass base64-encoded strings.",
api.Strings(["foo", "bar"]),
api.Calculator.Client.concat_method,
api.Calculator.Client.concat,
"foobar")

if failed > 0:
Expand Down
10 changes: 10 additions & 0 deletions libs/zswag/test/test_openapi_generator.bash
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,20 @@ while [[ $# -gt 0 ]]; do
esac
done

echo "→ [Test 1/2] Generate with auto-translation ..."
python -m zswag.gen \
--service calculator.Calculator \
--input "$my_dir/calc/calculator.zs" \
--config get,path,flat bitMul:post,body \
--config identity:put \
--output "$my_dir/.test.yaml"
diff -w "$my_dir/.test.yaml" "$my_dir/test_openapi_generator_1.yaml"

echo "→ [Test 2/2] Generate with Python source ..."
python -m zswag.gen \
--service calculator.Calculator \
--input "$(python -m zswag.test.calc path)" \
--config get,path,flat bitMul:post,body \
--config identity:put \
--output "$my_dir/.test.yaml"
diff -w "$my_dir/.test.yaml" "$my_dir/test_openapi_generator_2.yaml"
Loading

0 comments on commit 0629b10

Please sign in to comment.