Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
const auto &data = {{ request }};
USERVER_NAMESPACE::clients::http::Form form;

{% for field_name, field in body.schema.fields.items() %}
{% for field_name, field in body.fields().items() %}
{% if field.required %}
form.AddContent("{{ field_name }}", USERVER_NAMESPACE::chaotic::openapi::PrimitiveToString(data.{{ field_name }}));
{% else %}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{% macro serialize(request, http_request) %}
{{ http_request }}.data({{ request }}.data);
{# An operation with a single content type stores the body as a bare std::string,
while multiple content types are wrapped into distinct structs (see define_body_cpp_name)
to make the std::variant alternatives unambiguous. #}
{% macro serialize(request, http_request, wrapped) %}
{{ http_request }}.data({{ request }}{% if wrapped %}.data{% endif %});
{% endmacro %}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
const auto &data = {{ request }};
std::unordered_map<std::string, std::string> map;

{% for field_name, field in body.schema.fields.items() %}
{% for field_name, field in body.fields().items() %}
{% if field.required %}
map["{{ field_name }}"] = USERVER_NAMESPACE::chaotic::openapi::PrimitiveToString(data.{{ field_name }});
{% else %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,21 @@ namespace {{ namespace }} {

namespace openapi = USERVER_NAMESPACE::chaotic::openapi;

{% macro serialize_request_single_body(body, body_obj, http_request, wrapped) %}
{% if body.content_type != 'multipart/form-data' %}
sink.SetHeader(USERVER_NAMESPACE::http::headers::kContentType, "{{ body.content_type }}");
{% endif %}
{% if body.content_type == 'application/json' %}
{{ application_json.serialize(body_obj, http_request) }}
{% elif body.content_type == 'multipart/form-data' %}
{{ application_multipartformdata.serialize(body, body_obj, http_request) }}
{% elif body.content_type == 'application/x-www-form-urlencoded' %}
{{ application_xwwwformurlencoded.serialize(body, body_obj, http_request) }}
{% else %}
{{ application_octetstream.serialize(body_obj, http_request, wrapped=wrapped) }}
{% endif %}
{% endmacro %}

{% for op in operations %}
{% if op.client_generate %}
namespace {{ op.cpp_namespace() }} {
Expand Down Expand Up @@ -66,26 +81,13 @@ void SerializeRequest(const Request& request, const std::string& base_url, USERV

{# body #}
{% if len(op.request_bodies) == 1 %}
http_request.data(ToString(USERVER_NAMESPACE::formats::json::ValueBuilder(request.body).ExtractValue()));
{{ serialize_request_single_body(op.request_bodies[0], "request.body", "http_request", wrapped=False) }}
{% elif len(op.request_bodies) > 1 %}
switch (request.body.index()) {
{%- for num, body in enumerate(op.request_bodies) -%}
case {{ num }}: {
{% if body.content_type != 'multipart/form-data' %}
http_request.headers(USERVER_NAMESPACE::clients::http::Headers{ {USERVER_NAMESPACE::http::headers::kContentType, "{{ body.content_type }}"} });
{% endif %}

{% set body_obj = "std::get<" + str(num) + ">(request.body)" %}
{% if body.content_type == 'application/json' %}
{{ application_json.serialize(body_obj, "http_request") }}
{% elif body.content_type == 'multipart/form-data' %}
{{ application_multipartformdata.serialize(body, body_obj, "http_request") }}
{% elif body.content_type == 'application/x-www-form-urlencoded' %}
{{ application_xwwwformurlencoded.serialize(body, body_obj, "http_request") }}
{% else %}
{{ application_octetstream.serialize(body_obj, "http_request") }}
{% endif %}

{{ serialize_request_single_body(body, body_obj, "http_request", wrapped=True) }}
break;
}
{% endfor %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ def _validate_primitive_object(self, schema: cpp_types.CppType) -> None:
assert schema.json_schema
source_location = schema.json_schema.source_location()

schema = common_types.resolve_ref(schema)
if not isinstance(schema, cpp_types.CppStruct):
raise chaotic_error.BaseError(
full_filepath=source_location.filepath,
Expand All @@ -397,7 +398,7 @@ def _validate_primitive_object(self, schema: cpp_types.CppType) -> None:
msg='"application/x-www-form-urlencoded" body allows only "type: object"',
)
for field in schema.fields.values():
if not isinstance(field.schema, cpp_types.CppPrimitiveType):
if not isinstance(common_types.resolve_ref(field.schema), cpp_types.CppPrimitiveType):
raise chaotic_error.BaseError(
full_filepath=source_location.filepath,
infile_path=source_location.location,
Expand Down
14 changes: 14 additions & 0 deletions chaotic-openapi/chaotic_openapi/back/cpp/common/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
from chaotic_openapi.back.cpp.client import middleware


def resolve_ref(schema: cpp_types.CppType) -> cpp_types.CppType:
"""Follow the `$ref` chain down to the referenced type."""
while isinstance(schema, cpp_types.CppRef):
schema = schema.orig_cpp_type
return schema


@dataclasses.dataclass
class Security:
auth_type: str
Expand Down Expand Up @@ -134,6 +141,13 @@ def cpp_type(self) -> str:
assert self.schema is not None
return self.schema.cpp_user_name()

def fields(self) -> dict[str, cpp_types.CppStructField]:
"""Fields of a form body, with `$ref` resolved."""
assert self.schema is not None
schema = resolve_ref(self.schema)
assert isinstance(schema, cpp_types.CppStruct), schema
return schema.fields


@dataclasses.dataclass
class Response:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ namespace {{ spec.cpp_namespace }}::{{ op.cpp_namespace() }} {
{% endmacro %}

{% macro _parse_urlencoded(body, prefix) %}
{% for field_name, field in body.schema.fields.items() %}
{% for field_name, field in body.fields().items() %}
{% if field.required %}
if (!http_request.HasArg("{{ field_name }}")) {
throw {{ userver }}::server::handlers::ClientError(
Expand All @@ -49,7 +49,7 @@ namespace {{ spec.cpp_namespace }}::{{ op.cpp_namespace() }} {
{% endmacro %}

{% macro _parse_multipart(body, prefix) %}
{% for field_name, field in body.schema.fields.items() %}
{% for field_name, field in body.fields().items() %}
{% if field.required %}
if (!http_request.HasFormDataArg("{{ field_name }}")) {
throw {{ userver }}::server::handlers::ClientError(
Expand Down
21 changes: 13 additions & 8 deletions chaotic-openapi/chaotic_openapi/front/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,15 +182,20 @@ def _convert_swagger_request_body(
msg='"consumes" must be either "multipart/form-data" or "application/x-www-form-urlencoded" for "type: file"',
)

schema = self._parse_schema(
request_body.model_dump(
by_alias=True,
exclude={'name', 'in_', 'description', 'required', 'allowEmptyValue', 'collectionFormat'},
exclude_unset=True,
),
infile_path,
allow_file=True,
field_schema = request_body.model_dump(
by_alias=True,
exclude={'name', 'in_', 'description', 'required', 'allowEmptyValue', 'collectionFormat'},
exclude_unset=True,
)
object_schema: dict[str, Any] = {
'type': 'object',
'properties': {request_body.name: field_schema},
Comment thread
alex-aparin marked this conversation as resolved.
'additionalProperties': False,
}
if request_body.required:
object_schema['required'] = [request_body.name]

schema = self._parse_schema(object_schema, infile_path, allow_file=True)
return [
model.RequestBody(
content_type=mime,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ base_url + "/testme"
openapi::WriteParameter<openapi::TrivialParameter<openapi::In::kQuery, knumber, std::string, std::string>>(request.number, sink);
openapi::WriteParameter<openapi::ArrayParameter<openapi::In::kQuery, karray, ',', std::string, std::string>>(request.array, sink);

http_request.data(ToString(USERVER_NAMESPACE::formats::json::ValueBuilder(request.body).ExtractValue()));
sink.SetHeader(USERVER_NAMESPACE::http::headers::kContentType, "application/json");

http_request.data(ToString(USERVER_NAMESPACE::formats::json::ValueBuilder(request.body).ExtractValue()));

sink.Flush();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,54 +10,125 @@ paths:
content:
application/json:
schema:
type: object
properties:
foo:
type: string
additionalProperties: false
$ref: '#/components/schemas/single-json'
multipart/form-data:
schema:
type: object
required:
- filename
properties:
filename:
type: string
content:
type: string
additionalProperties: false
$ref: '#/components/schemas/single-form-data'
application/x-www-form-urlencoded:
schema:
type: object
required:
- name
- age
properties:
name:
type: string
password:
type: string
age:
type: integer
salary:
type: number
is_smoking:
type: boolean
additionalProperties: false
$ref: '#/components/schemas/single-form-urlen'
application/octet-stream:
schema:
type: string
$ref: '#/components/schemas/single-octet'
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
bar:
type: string
additionalProperties: false
$ref: '#/components/schemas/common-response'
application/octet-stream:
schema:
type: string
/test-single-json:
post:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/single-json'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/common-response'

/test-single-form-data:
post:
requestBody:
content:
multipart/form-data:
schema:
$ref: '#/components/schemas/single-form-data'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/common-response'

/test-single-form-urlen:
post:
requestBody:
content:
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/single-form-urlen'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/common-response'

/test-single-octet:
post:
requestBody:
content:
application/octet-stream:
schema:
$ref: '#/components/schemas/single-octet'
responses:
'200':
description: OK
content:
application/octet-stream:
schema:
type: string
components:
schemas:
single-json:
type: object
properties:
foo:
type: string
additionalProperties: false
single-form-data:
type: object
required:
- filename
properties:
filename:
type: string
content:
type: string
additionalProperties: false
single-form-urlen:
type: object
required:
- name
- age
properties:
name:
type: string
password:
type: string
age:
type: integer
salary:
type: number
is_smoking:
type: boolean
additionalProperties: false
single-octet:
type: string
common-response:
type: object
properties:
bar:
type: string
additionalProperties: false
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
swagger: '2.0'
description: |
Swagger "in: formData" request body.

Such a parameter describes a single form field, not the whole body, so it is
wrapped into a one-property object schema (see
Parser._convert_swagger_request_body). Only one "in: formData" parameter per
operation is supported for now, the rest are silently dropped by the parser.

paths:
/form-urlencoded:
post:
consumes:
- application/x-www-form-urlencoded
parameters:
- in: formData
name: name
required: true
type: string
responses:
'200':
description: OK
Loading
Loading