Skip to content
This repository was archived by the owner on Dec 25, 2024. It is now read-only.

Commit d3f89b7

Browse files
authored
Merge pull request #33 from openapi-json-schema-tools/improves_query_param_json_tests
Fixes query param content type json serialization
2 parents 7133ae8 + b5a64a5 commit d3f89b7

File tree

72 files changed

+1184
-340
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+1184
-340
lines changed

README.md

+10-6
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,23 @@ Currently, the following languages/frameworks are supported:
3636
- instantiating models
3737
- sending to endpoints
3838
- receiving from endpoints
39-
- Type hints for all model inputs
40-
- Type hints for accessing properties in object instances so some_val in some_val = some_inst['someKey'] will have the correct type hint
41-
- Type hints for accessing array items in array instances so some_val in some_val = array_inst[0] will have the correct type hint
42-
- Endpoints have input and response type hints
39+
- Type hints on
40+
- all model inputs in `__new__`
41+
- accessing properties in object instances so some_val in some_val = some_inst['someKey'] will have the correct type hint
42+
- accessing array items in array instances so some_val in some_val = array_inst[0] will have the correct type hint
43+
- endpoint inputs + responses
44+
- Format support for: int32, int64, float, double, binary, date, datetime, uuid
45+
- Invalid (in python) property names supported like `from`, `1var`, `hi-there` etc in
46+
- schema property names
47+
- endpoint parameter names
4348
- Openapi spec inline schemas supported at any depth
4449
- If needed, validation of some json schema keywords can be deactivated via a configuration class
4550
- Payload values are not coerced when validated, so a datetime value can pass other validations that describe the payload only as type string
4651
- String transmission of numbers supported with type: string, format: number, value can be accessed as a Decimal with inst.as_decimal_oapg
47-
- Format support for: int32, int64, float, double, binary, date, datetime
4852
- Multiple content types supported for request and response bodies
4953
- Endpoint response always also includes the urllib3.HTTPResponse
5054
- Endpoint response deserialization can be skipped with the skip_deserialization argument
51-
- Invalid (in python) property names supported like self, from etc
55+
- Validated payload instances subclass all validated schemas so no need to run validate twice, just use isinstance(some_inst, SomeSchemaClass)
5256

5357
And many more!
5458
- [Docs for the python generator](https://github.com/openapi-json-schema-tools/openapi-json-schema-generator/blob/master/docs/generators/python.md)

modules/openapi-json-schema-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -997,12 +997,13 @@ public CodegenParameter fromParameter(Parameter parameter, Set<String> imports)
997997
}
998998
}
999999
// clone this so we can change some properties on it
1000-
CodegenProperty schemaProp = cp.getSchema().clone();
1000+
CodegenProperty schemaProp = cp.getSchema();
10011001
// parameters may have valid python names like some_val or invalid ones like Content-Type
10021002
// we always set nameInSnakeCase to null so special handling will not be done for these names
10031003
// invalid python names will be handled in python by using a TypedDict which will allow us to have a type hint
10041004
// for keys that cannot be variable names to the schema baseName
10051005
if (schemaProp != null) {
1006+
schemaProp = schemaProp.clone();
10061007
schemaProp.nameInSnakeCase = null;
10071008
schemaProp.baseName = toModelName(cp.baseName) + "Schema";
10081009
cp.setSchema(schemaProp);

modules/openapi-json-schema-generator/src/main/resources/python/api_client.handlebars

+36-29
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ class RequestField(RequestFieldBase):
4949

5050

5151
class JSONEncoder(json.JSONEncoder):
52+
compact_separators = (',', ':')
53+
5254
def default(self, obj):
5355
if isinstance(obj, str):
5456
return str(obj)
@@ -320,8 +322,25 @@ class StyleSimpleSerializer(ParameterSerializerBase):
320322
)
321323

322324

325+
class JSONDetector:
326+
"""
327+
Works for:
328+
application/json
329+
application/json; charset=UTF-8
330+
application/json-patch+json
331+
application/geo+json
332+
"""
333+
__json_content_type_pattern = re.compile("application/[^+]*[+]?(json);?.*")
334+
335+
@classmethod
336+
def _content_type_is_json(cls, content_type: str) -> bool:
337+
if cls.__json_content_type_pattern.match(content_type):
338+
return True
339+
return False
340+
341+
323342
@dataclass
324-
class ParameterBase:
343+
class ParameterBase(JSONDetector):
325344
name: str
326345
in_type: ParameterInType
327346
required: bool
@@ -348,7 +367,6 @@ class ParameterBase:
348367
}
349368
__disallowed_header_names = {'Accept', 'Content-Type', 'Authorization'}
350369
_json_encoder = JSONEncoder()
351-
_json_content_type = 'application/json'
352370

353371
@classmethod
354372
def __verify_style_to_in_type(cls, style: typing.Optional[ParameterStyle], in_type: ParameterInType):
@@ -395,8 +413,11 @@ class ParameterBase:
395413

396414
def _serialize_json(
397415
self,
398-
in_data: typing.Union[None, int, float, str, bool, dict, list]
416+
in_data: typing.Union[None, int, float, str, bool, dict, list],
417+
eliminate_whitespace: bool = False
399418
) -> str:
419+
if eliminate_whitespace:
420+
return json.dumps(in_data, separators=self._json_encoder.compact_separators)
400421
return json.dumps(in_data)
401422

402423

@@ -491,7 +512,7 @@ class PathParameter(ParameterBase, StyleSimpleSerializer):
491512
for content_type, schema in self.content.items():
492513
cast_in_data = schema(in_data)
493514
cast_in_data = self._json_encoder.default(cast_in_data)
494-
if content_type == self._json_content_type:
515+
if self._content_type_is_json(content_type):
495516
value = self._serialize_json(cast_in_data)
496517
return self._to_dict(self.name, value)
497518
raise NotImplementedError('Serialization of {} has not yet been implemented'.format(content_type))
@@ -509,7 +530,7 @@ class QueryParameter(ParameterBase, StyleFormSerializer):
509530
schema: typing.Optional[typing.Type[Schema]] = None,
510531
content: typing.Optional[typing.Dict[str, typing.Type[Schema]]] = None
511532
):
512-
used_style = ParameterStyle.FORM if style is None and content is None and schema else style
533+
used_style = ParameterStyle.FORM if style is None else style
513534
used_explode = self._get_default_explode(used_style) if explode is None else explode
514535

515536
super().__init__(
@@ -572,8 +593,6 @@ class QueryParameter(ParameterBase, StyleFormSerializer):
572593
return self._to_dict(self.name, value)
573594

574595
def get_prefix_separator_iterator(self) -> typing.Optional[PrefixSeparatorIterator]:
575-
if not self.schema:
576-
return None
577596
if self.style is ParameterStyle.FORM:
578597
return PrefixSeparatorIterator('?', '&')
579598
elif self.style is ParameterStyle.SPACE_DELIMITED:
@@ -612,12 +631,17 @@ class QueryParameter(ParameterBase, StyleFormSerializer):
612631
elif self.style is ParameterStyle.PIPE_DELIMITED:
613632
return self.__serialize_pipe_delimited(cast_in_data, prefix_separator_iterator)
614633
# self.content will be length one
634+
if prefix_separator_iterator is None:
635+
prefix_separator_iterator = self.get_prefix_separator_iterator()
615636
for content_type, schema in self.content.items():
616637
cast_in_data = schema(in_data)
617638
cast_in_data = self._json_encoder.default(cast_in_data)
618-
if content_type == self._json_content_type:
619-
value = self._serialize_json(cast_in_data)
620-
return self._to_dict(self.name, value)
639+
if self._content_type_is_json(content_type):
640+
value = self._serialize_json(cast_in_data, eliminate_whitespace=True)
641+
return self._to_dict(
642+
self.name,
643+
next(prefix_separator_iterator) + self.name + '=' + quote(value)
644+
)
621645
raise NotImplementedError('Serialization of {} has not yet been implemented'.format(content_type))
622646

623647

@@ -676,7 +700,7 @@ class CookieParameter(ParameterBase, StyleFormSerializer):
676700
for content_type, schema in self.content.items():
677701
cast_in_data = schema(in_data)
678702
cast_in_data = self._json_encoder.default(cast_in_data)
679-
if content_type == self._json_content_type:
703+
if self._content_type_is_json(content_type):
680704
value = self._serialize_json(cast_in_data)
681705
return self._to_dict(self.name, value)
682706
raise NotImplementedError('Serialization of {} has not yet been implemented'.format(content_type))
@@ -733,7 +757,7 @@ class HeaderParameter(ParameterBase, StyleSimpleSerializer):
733757
for content_type, schema in self.content.items():
734758
cast_in_data = schema(in_data)
735759
cast_in_data = self._json_encoder.default(cast_in_data)
736-
if content_type == self._json_content_type:
760+
if self._content_type_is_json(content_type):
737761
value = self._serialize_json(cast_in_data)
738762
return self.__to_headers(((self.name, value),))
739763
raise NotImplementedError('Serialization of {} has not yet been implemented'.format(content_type))
@@ -796,23 +820,6 @@ class ApiResponseWithoutDeserialization(ApiResponse):
796820
headers: typing.Union[Unset, typing.List[HeaderParameter]] = unset
797821

798822

799-
class JSONDetector:
800-
"""
801-
Works for:
802-
application/json
803-
application/json; charset=UTF-8
804-
application/json-patch+json
805-
application/geo+json
806-
"""
807-
__json_content_type_pattern = re.compile("application/[^+]*[+]?(json);?.*")
808-
809-
@classmethod
810-
def _content_type_is_json(cls, content_type: str) -> bool:
811-
if cls.__json_content_type_pattern.match(content_type):
812-
return True
813-
return False
814-
815-
816823
class OpenApiResponse(JSONDetector):
817824
__filename_content_disposition_pattern = re.compile('filename="(.+?)"')
818825

modules/openapi-json-schema-generator/src/main/resources/python/endpoint.handlebars

+4-144
Original file line numberDiff line numberDiff line change
@@ -21,156 +21,16 @@ from . import path
2121
{{/unless}}
2222
{{#with operation}}
2323
{{#if queryParams}}
24-
# query params
25-
{{#each queryParams}}
26-
{{#with schema}}
27-
{{> model_templates/schema }}
28-
{{/with}}
29-
{{/each}}
30-
RequestRequiredQueryParams = typing_extensions.TypedDict(
31-
'RequestRequiredQueryParams',
32-
{
33-
{{#each queryParams}}
34-
{{#if required}}
35-
'{{baseName}}': {{#with schema}}typing.Union[{{baseName}}, {{> model_templates/schema_python_types }}],{{/with}}
36-
{{/if}}
37-
{{/each}}
38-
}
39-
)
40-
RequestOptionalQueryParams = typing_extensions.TypedDict(
41-
'RequestOptionalQueryParams',
42-
{
43-
{{#each queryParams}}
44-
{{#unless required}}
45-
'{{baseName}}': {{#with schema}}typing.Union[{{baseName}}, {{> model_templates/schema_python_types }}],{{/with}}
46-
{{/unless}}
47-
{{/each}}
48-
},
49-
total=False
50-
)
51-
52-
53-
class RequestQueryParams(RequestRequiredQueryParams, RequestOptionalQueryParams):
54-
pass
55-
56-
57-
{{#each queryParams}}
58-
{{> endpoint_parameter }}
59-
{{/each}}
24+
{{> endpoint_parameter_schema_and_def xParams=queryParams xParamsName="Query" }}
6025
{{/if}}
6126
{{#if headerParams}}
62-
# header params
63-
{{#each headerParams}}
64-
{{#with schema}}
65-
{{> model_templates/schema }}
66-
{{/with}}
67-
{{/each}}
68-
RequestRequiredHeaderParams = typing_extensions.TypedDict(
69-
'RequestRequiredHeaderParams',
70-
{
71-
{{#each headerParams}}
72-
{{#if required}}
73-
'{{baseName}}': {{#with schema}}typing.Union[{{baseName}}, {{> model_templates/schema_python_types }}],{{/with}}
74-
{{/if}}
75-
{{/each}}
76-
}
77-
)
78-
RequestOptionalHeaderParams = typing_extensions.TypedDict(
79-
'RequestOptionalHeaderParams',
80-
{
81-
{{#each headerParams}}
82-
{{#unless required}}
83-
'{{baseName}}': {{#with schema}}typing.Union[{{baseName}}, {{> model_templates/schema_python_types }}],{{/with}}
84-
{{/unless}}
85-
{{/each}}
86-
},
87-
total=False
88-
)
89-
90-
91-
class RequestHeaderParams(RequestRequiredHeaderParams, RequestOptionalHeaderParams):
92-
pass
93-
94-
95-
{{#each headerParams}}
96-
{{> endpoint_parameter }}
97-
{{/each}}
27+
{{> endpoint_parameter_schema_and_def xParams=headerParams xParamsName="Header" }}
9828
{{/if}}
9929
{{#if pathParams}}
100-
# path params
101-
{{#each pathParams}}
102-
{{#with schema}}
103-
{{> model_templates/schema }}
104-
{{/with}}
105-
{{/each}}
106-
RequestRequiredPathParams = typing_extensions.TypedDict(
107-
'RequestRequiredPathParams',
108-
{
109-
{{#each pathParams}}
110-
{{#if required}}
111-
'{{baseName}}': {{#with schema}}typing.Union[{{baseName}}, {{> model_templates/schema_python_types }}],{{/with}}
112-
{{/if}}
113-
{{/each}}
114-
}
115-
)
116-
RequestOptionalPathParams = typing_extensions.TypedDict(
117-
'RequestOptionalPathParams',
118-
{
119-
{{#each pathParams}}
120-
{{#unless required}}
121-
'{{baseName}}': {{#with schema}}typing.Union[{{baseName}}, {{> model_templates/schema_python_types }}],{{/with}}
122-
{{/unless}}
123-
{{/each}}
124-
},
125-
total=False
126-
)
127-
128-
129-
class RequestPathParams(RequestRequiredPathParams, RequestOptionalPathParams):
130-
pass
131-
132-
133-
{{#each pathParams}}
134-
{{> endpoint_parameter }}
135-
{{/each}}
30+
{{> endpoint_parameter_schema_and_def xParams=pathParams xParamsName="Path" }}
13631
{{/if}}
13732
{{#if cookieParams}}
138-
# cookie params
139-
{{#each cookieParams}}
140-
{{#with schema}}
141-
{{> model_templates/schema }}
142-
{{/with}}
143-
{{/each}}
144-
RequestRequiredCookieParams = typing_extensions.TypedDict(
145-
'RequestRequiredCookieParams',
146-
{
147-
{{#each cookieParams}}
148-
{{#if required}}
149-
'{{baseName}}': {{#with schema}}typing.Union[{{baseName}}, {{> model_templates/schema_python_types }}],{{/with}}
150-
{{/if}}
151-
{{/each}}
152-
}
153-
)
154-
RequestOptionalCookieParams = typing_extensions.TypedDict(
155-
'RequestOptionalCookieParams',
156-
{
157-
{{#each cookieParams}}
158-
{{#unless required}}
159-
'{{baseName}}': {{#with schema}}typing.Union[{{baseName}}, {{> model_templates/schema_python_types }}],{{/with}}
160-
{{/unless}}
161-
{{/each}}
162-
},
163-
total=False
164-
)
165-
166-
167-
class RequestCookieParams(RequestRequiredCookieParams, RequestOptionalCookieParams):
168-
pass
169-
170-
171-
{{#each cookieParams}}
172-
{{> endpoint_parameter }}
173-
{{/each}}
33+
{{> endpoint_parameter_schema_and_def xParams=cookieParams xParamsName="Cookie" }}
17434
{{/if}}
17535
{{#with bodyParam}}
17636
# body param

modules/openapi-json-schema-generator/src/main/resources/python/endpoint_parameter.handlebars

+9-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,16 @@ request_{{#if isQueryParam}}query{{/if}}{{#if isPathParam}}path{{/if}}{{#if isHe
44
style=api_client.ParameterStyle.{{style}},
55
{{/if}}
66
{{#if schema}}
7-
{{#with schema}}
7+
{{#with schema}}
88
schema={{baseName}},
9-
{{/with}}
9+
{{/with}}
10+
{{/if}}
11+
{{#if getContent}}
12+
content={
13+
{{#each getContent}}
14+
"{{@key}}": {{#with this}}{{#with schema}}{{baseName}}{{/with}}{{/with}},
15+
{{/each}}
16+
},
1017
{{/if}}
1118
{{#if required}}
1219
required=True,

0 commit comments

Comments
 (0)