geniespec 0.1.1.dev4__tar.gz → 0.1.1.dev5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {geniespec-0.1.1.dev4 → geniespec-0.1.1.dev5}/PKG-INFO +6 -5
- {geniespec-0.1.1.dev4 → geniespec-0.1.1.dev5}/README.md +5 -4
- {geniespec-0.1.1.dev4 → geniespec-0.1.1.dev5}/pyproject.toml +1 -1
- {geniespec-0.1.1.dev4 → geniespec-0.1.1.dev5}/pyproject.toml.orig +1 -1
- {geniespec-0.1.1.dev4 → geniespec-0.1.1.dev5}/src/geniespec/genie.py +4 -1
- {geniespec-0.1.1.dev4 → geniespec-0.1.1.dev5}/src/geniespec/languages.py +366 -129
- geniespec-0.1.1.dev5/src/geniespec/templates/typescript-client/{service}/v{version}/client.ts.j2 +41 -0
- geniespec-0.1.1.dev5/src/geniespec/templates/typescript-client/{service}/v{version}/endpoints.ts.j2 +59 -0
- geniespec-0.1.1.dev5/src/geniespec/templates/typescript-client/{service}/v{version}/index.ts.j2 +4 -0
- geniespec-0.1.1.dev5/src/geniespec/templates/typescript-client/{service}/v{version}/resources.ts.j2 +45 -0
- {geniespec-0.1.1.dev4 → geniespec-0.1.1.dev5}/src/geniespec/__init__.py +0 -0
- {geniespec-0.1.1.dev4 → geniespec-0.1.1.dev5}/src/geniespec/api_schema.yaml +0 -0
- {geniespec-0.1.1.dev4 → geniespec-0.1.1.dev5}/src/geniespec/cli.py +0 -0
- {geniespec-0.1.1.dev4 → geniespec-0.1.1.dev5}/src/geniespec/renderable_types.py +0 -0
- {geniespec-0.1.1.dev4 → geniespec-0.1.1.dev5}/src/geniespec/templates/python-server/{service}/__init__.py.j2 +0 -0
- {geniespec-0.1.1.dev4 → geniespec-0.1.1.dev5}/src/geniespec/templates/python-server/{service}/v{version}/__init__.py.j2 +0 -0
- {geniespec-0.1.1.dev4 → geniespec-0.1.1.dev5}/src/geniespec/templates/python-server/{service}/v{version}/api.py.j2 +0 -0
- {geniespec-0.1.1.dev4 → geniespec-0.1.1.dev5}/src/geniespec/templates/python-server/{service}/v{version}/endpoints.py.j2 +0 -0
- {geniespec-0.1.1.dev4 → geniespec-0.1.1.dev5}/src/geniespec/templates/python-server/{service}/v{version}/exceptions.py.j2 +0 -0
- {geniespec-0.1.1.dev4 → geniespec-0.1.1.dev5}/src/geniespec/templates/python-server/{service}/v{version}/internal.py.j2 +0 -0
- {geniespec-0.1.1.dev4 → geniespec-0.1.1.dev5}/src/geniespec/templates/python-server/{service}/v{version}/resources.py.j2 +0 -0
- {geniespec-0.1.1.dev4 → geniespec-0.1.1.dev5}/src/geniespec/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: geniespec
|
|
3
|
-
Version: 0.1.1.
|
|
3
|
+
Version: 0.1.1.dev5
|
|
4
4
|
Summary: Generate API code from an api.yaml spec
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Dist: jinja2>=3.1.6
|
|
@@ -17,13 +17,14 @@ Description-Content-Type: text/markdown
|
|
|
17
17
|
Genie (`geniespec` on PyPI) generates API code from an `api.yaml` spec. Install it like any other tool and run it from your build scripts; the generated code is gitignored, not committed.
|
|
18
18
|
|
|
19
19
|
```
|
|
20
|
-
geniespec build --spec api.yaml --target python-server [--output .]
|
|
21
|
-
geniespec watch --spec api.yaml --target python-server [--output .] [-- <command>]
|
|
20
|
+
geniespec build --spec api.yaml --target <python-server | typescript-client> [--output .]
|
|
21
|
+
geniespec watch --spec api.yaml --target <python-server | typescript-client> [--output .] [-- <command>]
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
- `build` writes the target's package into the output directory
|
|
24
|
+
- `build` writes the target's package into the output directory. Only files in that package are touched: changed files are rewritten and files it no longer generates are removed.
|
|
25
|
+
- `python-server`: `./<service>_api/v<major>/` (`api`, `endpoints`, `resources`, `internal`, `exceptions`). Code run from the output directory imports it directly, e.g. `from my_service_api.v1 import api`. Needs `kiba-core` (`core.api`, `core.util.typing_util`), `pydantic` and `starlette`.
|
|
26
|
+
- `typescript-client`: `./<service>-api/v<major>/` (`client`, `endpoints`, `resources`, `index`) with a `<Service>ApiV<major>Client` that extends `@kibalabs/core`'s `ServiceClient`. Construct it with the url the API version is mounted at, e.g. `new MyServiceApiV1Client(requester, 'https://api.example.com/v1')`. Methods take path parameters, then a params object for the request fields, and return the response field; stream transitions take a callback for each item. Transitions with `responseType: redirect` or a `query-parameters` field are left out, as browsers open redirects directly and query-parameters fields receive third-party callbacks. Needs `@kibalabs/core`.
|
|
25
27
|
- `watch` builds, then rebuilds whenever the spec changes. If the spec is invalid it logs the error and keeps the previous output. Given a command after `--`, it runs it once the first build is done, keeps rebuilding while it runs, exits with its exit code and stops it when watch itself is stopped, e.g. `geniespec watch --spec api.yaml --target python-server -- uvicorn app:app --reload`.
|
|
26
|
-
- Code run from the output directory imports the package directly, e.g. `from my_service_api.v1 import api`. Generated `python-server` code needs `kiba-core` (`core.api`, `core.util.typing_util`), `pydantic` and `starlette`.
|
|
27
28
|
- The bundled templates are examples of a kibalabs-style target.
|
|
28
29
|
|
|
29
30
|
# Development
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
Genie (`geniespec` on PyPI) generates API code from an `api.yaml` spec. Install it like any other tool and run it from your build scripts; the generated code is gitignored, not committed.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
|
-
geniespec build --spec api.yaml --target python-server [--output .]
|
|
7
|
-
geniespec watch --spec api.yaml --target python-server [--output .] [-- <command>]
|
|
6
|
+
geniespec build --spec api.yaml --target <python-server | typescript-client> [--output .]
|
|
7
|
+
geniespec watch --spec api.yaml --target <python-server | typescript-client> [--output .] [-- <command>]
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
-
- `build` writes the target's package into the output directory
|
|
10
|
+
- `build` writes the target's package into the output directory. Only files in that package are touched: changed files are rewritten and files it no longer generates are removed.
|
|
11
|
+
- `python-server`: `./<service>_api/v<major>/` (`api`, `endpoints`, `resources`, `internal`, `exceptions`). Code run from the output directory imports it directly, e.g. `from my_service_api.v1 import api`. Needs `kiba-core` (`core.api`, `core.util.typing_util`), `pydantic` and `starlette`.
|
|
12
|
+
- `typescript-client`: `./<service>-api/v<major>/` (`client`, `endpoints`, `resources`, `index`) with a `<Service>ApiV<major>Client` that extends `@kibalabs/core`'s `ServiceClient`. Construct it with the url the API version is mounted at, e.g. `new MyServiceApiV1Client(requester, 'https://api.example.com/v1')`. Methods take path parameters, then a params object for the request fields, and return the response field; stream transitions take a callback for each item. Transitions with `responseType: redirect` or a `query-parameters` field are left out, as browsers open redirects directly and query-parameters fields receive third-party callbacks. Needs `@kibalabs/core`.
|
|
11
13
|
- `watch` builds, then rebuilds whenever the spec changes. If the spec is invalid it logs the error and keeps the previous output. Given a command after `--`, it runs it once the first build is done, keeps rebuilding while it runs, exits with its exit code and stops it when watch itself is stopped, e.g. `geniespec watch --spec api.yaml --target python-server -- uvicorn app:app --reload`.
|
|
12
|
-
- Code run from the output directory imports the package directly, e.g. `from my_service_api.v1 import api`. Generated `python-server` code needs `kiba-core` (`core.api`, `core.util.typing_util`), `pydantic` and `starlette`.
|
|
13
14
|
- The bundled templates are examples of a kibalabs-style target.
|
|
14
15
|
|
|
15
16
|
# Development
|
|
@@ -14,6 +14,7 @@ from core.exceptions import KibaException
|
|
|
14
14
|
|
|
15
15
|
from geniespec.languages import BaseLanguageDefinition
|
|
16
16
|
from geniespec.languages import PythonLanguageDefinition
|
|
17
|
+
from geniespec.languages import TypeScriptLanguageDefinition
|
|
17
18
|
from geniespec.renderable_types import ActionEndpointComponent
|
|
18
19
|
from geniespec.renderable_types import AttributeEndpointComponent
|
|
19
20
|
from geniespec.renderable_types import EndpointContainer
|
|
@@ -46,7 +47,7 @@ from geniespec.types import ResponseTypes
|
|
|
46
47
|
|
|
47
48
|
SCHEMA_FILE_PATH = pathlib.Path(__file__).parent / 'api_schema.yaml'
|
|
48
49
|
TEMPLATES_DIRECTORY_PATH = pathlib.Path(__file__).parent / 'templates'
|
|
49
|
-
TARGET_LANGUAGE_CODES = {'python-server': 'python'}
|
|
50
|
+
TARGET_LANGUAGE_CODES = {'python-server': 'python', 'typescript-client': 'typescript'}
|
|
50
51
|
|
|
51
52
|
|
|
52
53
|
class SyntacticValidationException(BadRequestException):
|
|
@@ -72,6 +73,8 @@ class SyntacticApiValidator:
|
|
|
72
73
|
def get_language_definition(languageCode: str) -> BaseLanguageDefinition:
|
|
73
74
|
if languageCode == 'python':
|
|
74
75
|
return PythonLanguageDefinition()
|
|
76
|
+
if languageCode == 'typescript':
|
|
77
|
+
return TypeScriptLanguageDefinition()
|
|
75
78
|
raise KibaException(f'Unrecognized language definition code: {languageCode}')
|
|
76
79
|
|
|
77
80
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import collections
|
|
2
2
|
import keyword
|
|
3
|
+
import re
|
|
3
4
|
|
|
4
5
|
from core.exceptions import KibaException
|
|
5
6
|
|
|
@@ -9,6 +10,7 @@ from geniespec.renderable_types import MappingRenderableType
|
|
|
9
10
|
from geniespec.renderable_types import RenderableApi
|
|
10
11
|
from geniespec.renderable_types import RenderableEndpointComponent
|
|
11
12
|
from geniespec.renderable_types import RenderableException
|
|
13
|
+
from geniespec.renderable_types import RenderableField
|
|
12
14
|
from geniespec.renderable_types import RenderableOperation
|
|
13
15
|
from geniespec.renderable_types import RenderableParameter
|
|
14
16
|
from geniespec.renderable_types import RenderableResource
|
|
@@ -26,6 +28,7 @@ from geniespec.types import Version
|
|
|
26
28
|
class BaseLanguageDefinition:
|
|
27
29
|
FILE_EXTENSIONS: list[str]
|
|
28
30
|
AUTOGENERATED_NOTICE: str
|
|
31
|
+
LANGUAGE_NAME: str
|
|
29
32
|
|
|
30
33
|
def boolean_type(self, isNullable: bool) -> str:
|
|
31
34
|
raise NotImplementedError
|
|
@@ -75,7 +78,13 @@ class BaseLanguageDefinition:
|
|
|
75
78
|
def package_name(self, api: RenderableApi) -> str:
|
|
76
79
|
raise NotImplementedError
|
|
77
80
|
|
|
78
|
-
def
|
|
81
|
+
def operation_name(self, operation: RenderableOperation) -> str:
|
|
82
|
+
raise NotImplementedError
|
|
83
|
+
|
|
84
|
+
def path_parameter_segment(self, parameterName: str, isInteger: bool) -> str:
|
|
85
|
+
raise NotImplementedError
|
|
86
|
+
|
|
87
|
+
def is_valid_name(self, name: str) -> bool:
|
|
79
88
|
raise NotImplementedError
|
|
80
89
|
|
|
81
90
|
def _render_one_of_type_string(self, oneOfTypeString: str, isNullable: bool, apiVersion: Version) -> list[str]:
|
|
@@ -131,58 +140,6 @@ class BaseLanguageDefinition:
|
|
|
131
140
|
isNullable = False
|
|
132
141
|
return self.render_type(renderableType=renderableParameter.fieldType.model_copy(update={'isNullable': isNullable}), apiVersion=apiVersion)
|
|
133
142
|
|
|
134
|
-
|
|
135
|
-
class PythonLanguageDefinition(BaseLanguageDefinition):
|
|
136
|
-
AUTOGENERATED_NOTICE = '# --- THIS FILE HAS BEEN AUTOGENERATED, DO NOT MODIFY MANUALLY ---'
|
|
137
|
-
|
|
138
|
-
def boolean_type(self, isNullable: bool) -> str:
|
|
139
|
-
return 'bool | None' if isNullable else 'bool'
|
|
140
|
-
|
|
141
|
-
def datetime_type(self, isNullable: bool) -> str:
|
|
142
|
-
return 'datetime.datetime | None' if isNullable else 'datetime.datetime'
|
|
143
|
-
|
|
144
|
-
def date_type(self, isNullable: bool) -> str:
|
|
145
|
-
return 'datetime.date | None' if isNullable else 'datetime.date'
|
|
146
|
-
|
|
147
|
-
def double_type(self, isNullable: bool) -> str:
|
|
148
|
-
return 'float | None' if isNullable else 'float'
|
|
149
|
-
|
|
150
|
-
def float_type(self, isNullable: bool) -> str:
|
|
151
|
-
return 'float | None' if isNullable else 'float'
|
|
152
|
-
|
|
153
|
-
def integer_type(self, isNullable: bool) -> str:
|
|
154
|
-
return 'int | None' if isNullable else 'int'
|
|
155
|
-
|
|
156
|
-
def mapping_type(self, isNullable: bool, keyType: str, valueType: str) -> str:
|
|
157
|
-
return f'collections.abc.Mapping[{keyType}, {valueType}] | None' if isNullable else f'collections.abc.Mapping[{keyType}, {valueType}]'
|
|
158
|
-
|
|
159
|
-
def json_type(self, isNullable: bool) -> str:
|
|
160
|
-
return 'pydantic.JsonValue | None' if isNullable else 'pydantic.JsonValue'
|
|
161
|
-
|
|
162
|
-
def list_type(self, isNullable: bool, listItemType: str) -> str:
|
|
163
|
-
return f'collections.abc.Sequence[{listItemType}] | None' if isNullable else f'collections.abc.Sequence[{listItemType}]'
|
|
164
|
-
|
|
165
|
-
def struct_type(self, isNullable: bool) -> str:
|
|
166
|
-
return 'typing.JsonMapping | None' if isNullable else 'JsonMapping'
|
|
167
|
-
|
|
168
|
-
def long_integer_type(self, isNullable: bool) -> str:
|
|
169
|
-
return 'int | None' if isNullable else 'int'
|
|
170
|
-
|
|
171
|
-
def object_type(self, isNullable: bool, className: RenderableString, apiVersion: Version, isLocalReference: bool) -> str:
|
|
172
|
-
typeName = f'{className.pascalCaseString}V{apiVersion.major}' if isLocalReference else f'resources.{className.pascalCaseString}V{apiVersion.major}'
|
|
173
|
-
return f'{typeName} | None' if isNullable else typeName
|
|
174
|
-
|
|
175
|
-
def one_of_type(self, isNullable: bool, subTypes: list[str]) -> str:
|
|
176
|
-
fieldTypes = ' | '.join(subTypes)
|
|
177
|
-
return f'{fieldTypes} | None' if isNullable else f'Union[{fieldTypes} | None'
|
|
178
|
-
|
|
179
|
-
def string_type(self, isNullable: bool) -> str:
|
|
180
|
-
return 'str | None' if isNullable else 'str'
|
|
181
|
-
|
|
182
|
-
def enum_type(self, isNullable: bool, values: list[str]) -> str:
|
|
183
|
-
valueString = ', '.join([f"'{value}'" for value in values])
|
|
184
|
-
return f'typing.Literal[{valueString}] | None' if isNullable else f'Literal[{valueString} | None'
|
|
185
|
-
|
|
186
143
|
@staticmethod
|
|
187
144
|
def _normalized_name(name: RenderableString) -> RenderableString:
|
|
188
145
|
if len(name.string) != 1 or name.string[0].isupper():
|
|
@@ -208,12 +165,6 @@ class PythonLanguageDefinition(BaseLanguageDefinition):
|
|
|
208
165
|
def kebab_name(cls, name: RenderableString) -> str:
|
|
209
166
|
return cls._normalized_name(name=name).kebabCaseString
|
|
210
167
|
|
|
211
|
-
def package_name(self, api: RenderableApi) -> str:
|
|
212
|
-
return f'{self.snake_name(name=api.serviceName)}_api'
|
|
213
|
-
|
|
214
|
-
def internal_class_name(self, api: RenderableApi) -> str:
|
|
215
|
-
return f'{self.pascal_name(name=api.serviceName)}ApiV{api.version.major}Internal'
|
|
216
|
-
|
|
217
168
|
def resource_class_name(self, resource: RenderableResource, apiVersion: Version) -> str:
|
|
218
169
|
return f'{self.pascal_name(name=resource.singleName)}V{apiVersion.major}'
|
|
219
170
|
|
|
@@ -226,17 +177,6 @@ class PythonLanguageDefinition(BaseLanguageDefinition):
|
|
|
226
177
|
def parameter_type(self, renderableParameter: RenderableParameter, apiVersion: Version) -> str:
|
|
227
178
|
return self.render_parameter(renderableParameter=renderableParameter, apiVersion=apiVersion)
|
|
228
179
|
|
|
229
|
-
@staticmethod
|
|
230
|
-
def route_auth(operation: RenderableOperation) -> str | None:
|
|
231
|
-
return operation.authentication if operation.authentication not in (None, 'none') else None
|
|
232
|
-
|
|
233
|
-
@classmethod
|
|
234
|
-
def requires_auth_resolver(cls, api: RenderableApi) -> bool:
|
|
235
|
-
return any(cls.route_auth(operation=operation) is not None for operation in api.operations)
|
|
236
|
-
|
|
237
|
-
def operation_name(self, operation: RenderableOperation) -> str:
|
|
238
|
-
return self.snake_name(name=self._operation_name(operation=operation))
|
|
239
|
-
|
|
240
180
|
def operation_class_name(self, operation: RenderableOperation) -> str:
|
|
241
181
|
return self.pascal_name(name=self._operation_name(operation=operation))
|
|
242
182
|
|
|
@@ -246,15 +186,6 @@ class PythonLanguageDefinition(BaseLanguageDefinition):
|
|
|
246
186
|
def is_undefined_parameter(self, operation: RenderableOperation, parameter: RenderableParameter) -> bool:
|
|
247
187
|
return self.http_method(operation=operation) == 'PATCH' and parameter.source == FieldSources.REQUEST and not parameter.isRequired and parameter.defaultValue is None
|
|
248
188
|
|
|
249
|
-
def requires_parameter_preprocessing(self, operation: RenderableOperation, parameter: RenderableParameter) -> bool:
|
|
250
|
-
return (parameter.defaultValue is not None and not self.is_undefined_parameter(operation=operation, parameter=parameter)) or parameter.minValue is not None or parameter.maxValue is not None
|
|
251
|
-
|
|
252
|
-
def requires_undefined(self, api: RenderableApi) -> bool:
|
|
253
|
-
return any(self.is_undefined_parameter(operation=operation, parameter=parameter) for operation in api.operations for parameter in operation.parameters)
|
|
254
|
-
|
|
255
|
-
def requires_optional_field_default(self, api: RenderableApi) -> bool:
|
|
256
|
-
return any(not self.is_undefined_parameter(operation=operation, parameter=parameter) and not parameter.isRequired and not parameter.fieldType.isNullable for operation in api.operations for parameter in self.request_parameters(operation=operation))
|
|
257
|
-
|
|
258
189
|
@staticmethod
|
|
259
190
|
def request_parameters(operation: RenderableOperation) -> list[RenderableParameter]:
|
|
260
191
|
return [parameter for parameter in operation.parameters if parameter.source == FieldSources.REQUEST]
|
|
@@ -267,52 +198,9 @@ class PythonLanguageDefinition(BaseLanguageDefinition):
|
|
|
267
198
|
def is_redirect(operation: RenderableOperation) -> bool:
|
|
268
199
|
return operation.responseType == ResponseTypes.REDIRECT
|
|
269
200
|
|
|
270
|
-
def requires_streaming(self, api: RenderableApi) -> bool:
|
|
271
|
-
return any(self.is_streaming(operation=operation) for operation in api.operations)
|
|
272
|
-
|
|
273
|
-
def requires_redirect(self, api: RenderableApi) -> bool:
|
|
274
|
-
return any(self.is_redirect(operation=operation) for operation in api.operations)
|
|
275
|
-
|
|
276
|
-
def return_type(self, operation: RenderableOperation, apiVersion: Version) -> str:
|
|
277
|
-
if operation.responseType == ResponseTypes.REDIRECT:
|
|
278
|
-
return 'str'
|
|
279
|
-
if not operation.responseFields:
|
|
280
|
-
return 'None'
|
|
281
|
-
fieldType = self.type(renderableType=operation.responseFields[0].fieldType, apiVersion=apiVersion)
|
|
282
|
-
return f'collections.abc.AsyncIterator[{fieldType}]' if operation.responseType == ResponseTypes.STREAM else fieldType
|
|
283
|
-
|
|
284
|
-
def parameter_value(self, operation: RenderableOperation, parameter: RenderableParameter) -> str:
|
|
285
|
-
if parameter.source == FieldSources.QUERY_PARAMETERS:
|
|
286
|
-
return 'dict(request.query_params)'
|
|
287
|
-
parameterName = self.identifier(name=parameter.name)
|
|
288
|
-
return parameterName if self.requires_parameter_preprocessing(operation=operation, parameter=parameter) else f'request.data.{parameterName}'
|
|
289
|
-
|
|
290
|
-
@staticmethod
|
|
291
|
-
def _contains_json(renderableType: RenderableType) -> bool:
|
|
292
|
-
if isinstance(renderableType, MappingRenderableType):
|
|
293
|
-
return PythonLanguageDefinition._contains_json(renderableType=renderableType.keyType) or PythonLanguageDefinition._contains_json(renderableType=renderableType.valueType)
|
|
294
|
-
return renderableType.rawType == PrimitiveTypes.JSON
|
|
295
|
-
|
|
296
|
-
def resources_require_json(self, api: RenderableApi) -> bool:
|
|
297
|
-
return any(self._contains_json(renderableType=attribute.fieldType) for resource in api.resources for attribute in resource.attributes)
|
|
298
|
-
|
|
299
|
-
def operations_require_json(self, api: RenderableApi) -> bool:
|
|
300
|
-
fieldTypes = [parameter.fieldType for operation in api.operations for parameter in operation.parameters] + [field.fieldType for operation in api.operations for field in operation.responseFields or []]
|
|
301
|
-
return any(self._contains_json(renderableType=fieldType) for fieldType in fieldTypes)
|
|
302
|
-
|
|
303
|
-
def exceptions_require_json(self, api: RenderableApi) -> bool:
|
|
304
|
-
return any(self._contains_json(renderableType=field.fieldType) for exception in api.exceptions for field in exception.fields)
|
|
305
|
-
|
|
306
201
|
def endpoint(self, operation: RenderableOperation) -> str:
|
|
307
202
|
return '/' + '/'.join([segment for component in operation.endpoint.components for segment in self._endpoint_segments(component=component)])
|
|
308
203
|
|
|
309
|
-
def route_ordered_operations(self, api: RenderableApi) -> list[RenderableOperation]:
|
|
310
|
-
return sorted(api.operations, key=lambda operation: tuple(segment.startswith('{') for segment in self.endpoint(operation=operation).split('/')))
|
|
311
|
-
|
|
312
|
-
@staticmethod
|
|
313
|
-
def literal(value: str | float | bool) -> str:
|
|
314
|
-
return repr(value)
|
|
315
|
-
|
|
316
204
|
def _endpoint_segments(self, component: RenderableEndpointComponent) -> list[str]:
|
|
317
205
|
if isinstance(component, AttributeEndpointComponent):
|
|
318
206
|
return [self.kebab_name(name=component.attribute.name)] if component.attribute is not None else ['attributes']
|
|
@@ -326,13 +214,9 @@ class PythonLanguageDefinition(BaseLanguageDefinition):
|
|
|
326
214
|
return output
|
|
327
215
|
if component.attribute.isUnique and not component.attribute.isPrimary:
|
|
328
216
|
output.append(self.kebab_name(name=component.attribute.name))
|
|
329
|
-
|
|
330
|
-
if component.attribute.fieldType.rawType in (PrimitiveTypes.INTEGER, PrimitiveTypes.LONG_INTEGER):
|
|
331
|
-
output.append(f'{{{parameterName}:int}}')
|
|
332
|
-
elif component.attribute.fieldType.rawType == PrimitiveTypes.STRING:
|
|
333
|
-
output.append(f'{{{parameterName}:str}}')
|
|
334
|
-
else:
|
|
217
|
+
if component.attribute.fieldType.rawType not in (PrimitiveTypes.INTEGER, PrimitiveTypes.LONG_INTEGER, PrimitiveTypes.STRING):
|
|
335
218
|
raise KibaException(message=f'ResourceEndpointComponent created with an invalid attribute type of {component.attribute.fieldType}. Valid types are: integer, long integer and string.')
|
|
219
|
+
output.append(self.path_parameter_segment(parameterName=self.identifier(name=component.attribute.name), isInteger=component.attribute.fieldType.rawType != PrimitiveTypes.STRING))
|
|
336
220
|
return output
|
|
337
221
|
raise KibaException(message=f'Unsupported endpoint component: {type(component)}')
|
|
338
222
|
|
|
@@ -398,5 +282,358 @@ class PythonLanguageDefinition(BaseLanguageDefinition):
|
|
|
398
282
|
namesWithContext += [(self.identifier(name=field.name), f'response field of {route}') for operation, route in zip(api.operations, routes, strict=True) for field in operation.responseFields or []]
|
|
399
283
|
namesWithContext += [(self.identifier(name=attribute.name), f'attribute of {resource.singleName.pascalCaseString}') for resource in api.resources for attribute in [*resource.attributes, *resource.independentAttributes]]
|
|
400
284
|
namesWithContext += [(self.identifier(name=field.name), f'field of {exception.name.pascalCaseString}') for exception in api.exceptions for field in exception.fields]
|
|
401
|
-
errors += [f'"{name}" is not a valid
|
|
285
|
+
errors += [f'"{name}" is not a valid {self.LANGUAGE_NAME} name ({context})' for name, context in namesWithContext if not self.is_valid_name(name=name)]
|
|
402
286
|
return errors
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class PythonLanguageDefinition(BaseLanguageDefinition):
|
|
290
|
+
AUTOGENERATED_NOTICE = '# --- THIS FILE HAS BEEN AUTOGENERATED, DO NOT MODIFY MANUALLY ---'
|
|
291
|
+
LANGUAGE_NAME = 'Python'
|
|
292
|
+
|
|
293
|
+
def boolean_type(self, isNullable: bool) -> str:
|
|
294
|
+
return 'bool | None' if isNullable else 'bool'
|
|
295
|
+
|
|
296
|
+
def datetime_type(self, isNullable: bool) -> str:
|
|
297
|
+
return 'datetime.datetime | None' if isNullable else 'datetime.datetime'
|
|
298
|
+
|
|
299
|
+
def date_type(self, isNullable: bool) -> str:
|
|
300
|
+
return 'datetime.date | None' if isNullable else 'datetime.date'
|
|
301
|
+
|
|
302
|
+
def double_type(self, isNullable: bool) -> str:
|
|
303
|
+
return 'float | None' if isNullable else 'float'
|
|
304
|
+
|
|
305
|
+
def float_type(self, isNullable: bool) -> str:
|
|
306
|
+
return 'float | None' if isNullable else 'float'
|
|
307
|
+
|
|
308
|
+
def integer_type(self, isNullable: bool) -> str:
|
|
309
|
+
return 'int | None' if isNullable else 'int'
|
|
310
|
+
|
|
311
|
+
def mapping_type(self, isNullable: bool, keyType: str, valueType: str) -> str:
|
|
312
|
+
return f'collections.abc.Mapping[{keyType}, {valueType}] | None' if isNullable else f'collections.abc.Mapping[{keyType}, {valueType}]'
|
|
313
|
+
|
|
314
|
+
def json_type(self, isNullable: bool) -> str:
|
|
315
|
+
return 'pydantic.JsonValue | None' if isNullable else 'pydantic.JsonValue'
|
|
316
|
+
|
|
317
|
+
def list_type(self, isNullable: bool, listItemType: str) -> str:
|
|
318
|
+
return f'collections.abc.Sequence[{listItemType}] | None' if isNullable else f'collections.abc.Sequence[{listItemType}]'
|
|
319
|
+
|
|
320
|
+
def struct_type(self, isNullable: bool) -> str:
|
|
321
|
+
return 'typing.JsonMapping | None' if isNullable else 'JsonMapping'
|
|
322
|
+
|
|
323
|
+
def long_integer_type(self, isNullable: bool) -> str:
|
|
324
|
+
return 'int | None' if isNullable else 'int'
|
|
325
|
+
|
|
326
|
+
def object_type(self, isNullable: bool, className: RenderableString, apiVersion: Version, isLocalReference: bool) -> str:
|
|
327
|
+
typeName = f'{className.pascalCaseString}V{apiVersion.major}' if isLocalReference else f'resources.{className.pascalCaseString}V{apiVersion.major}'
|
|
328
|
+
return f'{typeName} | None' if isNullable else typeName
|
|
329
|
+
|
|
330
|
+
def one_of_type(self, isNullable: bool, subTypes: list[str]) -> str:
|
|
331
|
+
fieldTypes = ' | '.join(subTypes)
|
|
332
|
+
return f'{fieldTypes} | None' if isNullable else f'Union[{fieldTypes} | None'
|
|
333
|
+
|
|
334
|
+
def string_type(self, isNullable: bool) -> str:
|
|
335
|
+
return 'str | None' if isNullable else 'str'
|
|
336
|
+
|
|
337
|
+
def enum_type(self, isNullable: bool, values: list[str]) -> str:
|
|
338
|
+
valueString = ', '.join([f"'{value}'" for value in values])
|
|
339
|
+
return f'typing.Literal[{valueString}] | None' if isNullable else f'Literal[{valueString} | None'
|
|
340
|
+
|
|
341
|
+
def package_name(self, api: RenderableApi) -> str:
|
|
342
|
+
return f'{self.snake_name(name=api.serviceName)}_api'
|
|
343
|
+
|
|
344
|
+
def internal_class_name(self, api: RenderableApi) -> str:
|
|
345
|
+
return f'{self.pascal_name(name=api.serviceName)}ApiV{api.version.major}Internal'
|
|
346
|
+
|
|
347
|
+
@staticmethod
|
|
348
|
+
def route_auth(operation: RenderableOperation) -> str | None:
|
|
349
|
+
return operation.authentication if operation.authentication not in (None, 'none') else None
|
|
350
|
+
|
|
351
|
+
@classmethod
|
|
352
|
+
def requires_auth_resolver(cls, api: RenderableApi) -> bool:
|
|
353
|
+
return any(cls.route_auth(operation=operation) is not None for operation in api.operations)
|
|
354
|
+
|
|
355
|
+
def operation_name(self, operation: RenderableOperation) -> str:
|
|
356
|
+
return self.snake_name(name=self._operation_name(operation=operation))
|
|
357
|
+
|
|
358
|
+
def requires_parameter_preprocessing(self, operation: RenderableOperation, parameter: RenderableParameter) -> bool:
|
|
359
|
+
return (parameter.defaultValue is not None and not self.is_undefined_parameter(operation=operation, parameter=parameter)) or parameter.minValue is not None or parameter.maxValue is not None
|
|
360
|
+
|
|
361
|
+
def requires_undefined(self, api: RenderableApi) -> bool:
|
|
362
|
+
return any(self.is_undefined_parameter(operation=operation, parameter=parameter) for operation in api.operations for parameter in operation.parameters)
|
|
363
|
+
|
|
364
|
+
def requires_optional_field_default(self, api: RenderableApi) -> bool:
|
|
365
|
+
return any(not self.is_undefined_parameter(operation=operation, parameter=parameter) and not parameter.isRequired and not parameter.fieldType.isNullable for operation in api.operations for parameter in self.request_parameters(operation=operation))
|
|
366
|
+
|
|
367
|
+
def requires_streaming(self, api: RenderableApi) -> bool:
|
|
368
|
+
return any(self.is_streaming(operation=operation) for operation in api.operations)
|
|
369
|
+
|
|
370
|
+
def requires_redirect(self, api: RenderableApi) -> bool:
|
|
371
|
+
return any(self.is_redirect(operation=operation) for operation in api.operations)
|
|
372
|
+
|
|
373
|
+
def return_type(self, operation: RenderableOperation, apiVersion: Version) -> str:
|
|
374
|
+
if operation.responseType == ResponseTypes.REDIRECT:
|
|
375
|
+
return 'str'
|
|
376
|
+
if not operation.responseFields:
|
|
377
|
+
return 'None'
|
|
378
|
+
fieldType = self.type(renderableType=operation.responseFields[0].fieldType, apiVersion=apiVersion)
|
|
379
|
+
return f'collections.abc.AsyncIterator[{fieldType}]' if operation.responseType == ResponseTypes.STREAM else fieldType
|
|
380
|
+
|
|
381
|
+
def parameter_value(self, operation: RenderableOperation, parameter: RenderableParameter) -> str:
|
|
382
|
+
if parameter.source == FieldSources.QUERY_PARAMETERS:
|
|
383
|
+
return 'dict(request.query_params)'
|
|
384
|
+
parameterName = self.identifier(name=parameter.name)
|
|
385
|
+
return parameterName if self.requires_parameter_preprocessing(operation=operation, parameter=parameter) else f'request.data.{parameterName}'
|
|
386
|
+
|
|
387
|
+
@staticmethod
|
|
388
|
+
def _contains_json(renderableType: RenderableType) -> bool:
|
|
389
|
+
if isinstance(renderableType, MappingRenderableType):
|
|
390
|
+
return PythonLanguageDefinition._contains_json(renderableType=renderableType.keyType) or PythonLanguageDefinition._contains_json(renderableType=renderableType.valueType)
|
|
391
|
+
return renderableType.rawType == PrimitiveTypes.JSON
|
|
392
|
+
|
|
393
|
+
def resources_require_json(self, api: RenderableApi) -> bool:
|
|
394
|
+
return any(self._contains_json(renderableType=attribute.fieldType) for resource in api.resources for attribute in resource.attributes)
|
|
395
|
+
|
|
396
|
+
def operations_require_json(self, api: RenderableApi) -> bool:
|
|
397
|
+
fieldTypes = [parameter.fieldType for operation in api.operations for parameter in operation.parameters] + [field.fieldType for operation in api.operations for field in operation.responseFields or []]
|
|
398
|
+
return any(self._contains_json(renderableType=fieldType) for fieldType in fieldTypes)
|
|
399
|
+
|
|
400
|
+
def exceptions_require_json(self, api: RenderableApi) -> bool:
|
|
401
|
+
return any(self._contains_json(renderableType=field.fieldType) for exception in api.exceptions for field in exception.fields)
|
|
402
|
+
|
|
403
|
+
def route_ordered_operations(self, api: RenderableApi) -> list[RenderableOperation]:
|
|
404
|
+
return sorted(api.operations, key=lambda operation: tuple(segment.startswith('{') for segment in self.endpoint(operation=operation).split('/')))
|
|
405
|
+
|
|
406
|
+
@staticmethod
|
|
407
|
+
def literal(value: str | float | bool) -> str:
|
|
408
|
+
return repr(value)
|
|
409
|
+
|
|
410
|
+
def path_parameter_segment(self, parameterName: str, isInteger: bool) -> str:
|
|
411
|
+
return f'{{{parameterName}:int}}' if isInteger else f'{{{parameterName}:str}}'
|
|
412
|
+
|
|
413
|
+
def is_valid_name(self, name: str) -> bool:
|
|
414
|
+
return name.isidentifier() and not keyword.iskeyword(name)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
class TypeScriptLanguageDefinition(BaseLanguageDefinition):
|
|
418
|
+
AUTOGENERATED_NOTICE = '// --- THIS FILE HAS BEEN AUTOGENERATED, DO NOT MODIFY MANUALLY ---'
|
|
419
|
+
LANGUAGE_NAME = 'TypeScript'
|
|
420
|
+
# NOTE(krishan711): names are used as constructor and method parameters, so they can't be words reserved in strict mode modules
|
|
421
|
+
RESERVED_WORDS = frozenset(
|
|
422
|
+
{
|
|
423
|
+
'arguments',
|
|
424
|
+
'await',
|
|
425
|
+
'break',
|
|
426
|
+
'case',
|
|
427
|
+
'catch',
|
|
428
|
+
'class',
|
|
429
|
+
'const',
|
|
430
|
+
'continue',
|
|
431
|
+
'debugger',
|
|
432
|
+
'default',
|
|
433
|
+
'delete',
|
|
434
|
+
'do',
|
|
435
|
+
'else',
|
|
436
|
+
'enum',
|
|
437
|
+
'eval',
|
|
438
|
+
'export',
|
|
439
|
+
'extends',
|
|
440
|
+
'false',
|
|
441
|
+
'finally',
|
|
442
|
+
'for',
|
|
443
|
+
'function',
|
|
444
|
+
'if',
|
|
445
|
+
'implements',
|
|
446
|
+
'import',
|
|
447
|
+
'in',
|
|
448
|
+
'instanceof',
|
|
449
|
+
'interface',
|
|
450
|
+
'let',
|
|
451
|
+
'new',
|
|
452
|
+
'null',
|
|
453
|
+
'package',
|
|
454
|
+
'private',
|
|
455
|
+
'protected',
|
|
456
|
+
'public',
|
|
457
|
+
'return',
|
|
458
|
+
'static',
|
|
459
|
+
'super',
|
|
460
|
+
'switch',
|
|
461
|
+
'this',
|
|
462
|
+
'throw',
|
|
463
|
+
'true',
|
|
464
|
+
'try',
|
|
465
|
+
'typeof',
|
|
466
|
+
'var',
|
|
467
|
+
'void',
|
|
468
|
+
'while',
|
|
469
|
+
'with',
|
|
470
|
+
'yield',
|
|
471
|
+
}
|
|
472
|
+
)
|
|
473
|
+
QUERY_METHODS = frozenset({'GET', 'DELETE'})
|
|
474
|
+
|
|
475
|
+
@staticmethod
|
|
476
|
+
def _nullable(typeName: str, isNullable: bool) -> str:
|
|
477
|
+
return f'{typeName} | null' if isNullable else typeName
|
|
478
|
+
|
|
479
|
+
def boolean_type(self, isNullable: bool) -> str:
|
|
480
|
+
return self._nullable(typeName='boolean', isNullable=isNullable)
|
|
481
|
+
|
|
482
|
+
def datetime_type(self, isNullable: bool) -> str:
|
|
483
|
+
return self._nullable(typeName='Date', isNullable=isNullable)
|
|
484
|
+
|
|
485
|
+
def date_type(self, isNullable: bool) -> str:
|
|
486
|
+
return self._nullable(typeName='Date', isNullable=isNullable)
|
|
487
|
+
|
|
488
|
+
def double_type(self, isNullable: bool) -> str:
|
|
489
|
+
return self._nullable(typeName='number', isNullable=isNullable)
|
|
490
|
+
|
|
491
|
+
def float_type(self, isNullable: bool) -> str:
|
|
492
|
+
return self._nullable(typeName='number', isNullable=isNullable)
|
|
493
|
+
|
|
494
|
+
def integer_type(self, isNullable: bool) -> str:
|
|
495
|
+
return self._nullable(typeName='number', isNullable=isNullable)
|
|
496
|
+
|
|
497
|
+
def long_integer_type(self, isNullable: bool) -> str:
|
|
498
|
+
return self._nullable(typeName='number', isNullable=isNullable)
|
|
499
|
+
|
|
500
|
+
def mapping_type(self, isNullable: bool, keyType: str, valueType: str) -> str: # noqa: ARG002
|
|
501
|
+
# NOTE(krishan711): json object keys are always strings
|
|
502
|
+
return self._nullable(typeName=f'Record<string, {valueType}>', isNullable=isNullable)
|
|
503
|
+
|
|
504
|
+
def render_type(self, renderableType: RenderableType, apiVersion: Version, isLocalReference: bool = False) -> str:
|
|
505
|
+
if renderableType.rawType == PrimitiveTypes.JSON and not renderableType.isList:
|
|
506
|
+
return self._nullable(typeName='JsonValue' if isLocalReference else 'Resources.JsonValue', isNullable=renderableType.isNullable)
|
|
507
|
+
return super().render_type(renderableType=renderableType, apiVersion=apiVersion, isLocalReference=isLocalReference)
|
|
508
|
+
|
|
509
|
+
def list_type(self, isNullable: bool, listItemType: str) -> str:
|
|
510
|
+
return self._nullable(typeName=f'{listItemType}[]', isNullable=isNullable)
|
|
511
|
+
|
|
512
|
+
def object_type(self, isNullable: bool, className: RenderableString, apiVersion: Version, isLocalReference: bool) -> str:
|
|
513
|
+
typeName = f'{className.pascalCaseString}V{apiVersion.major}'
|
|
514
|
+
return self._nullable(typeName=typeName if isLocalReference else f'Resources.{typeName}', isNullable=isNullable)
|
|
515
|
+
|
|
516
|
+
def string_type(self, isNullable: bool) -> str:
|
|
517
|
+
return self._nullable(typeName='string', isNullable=isNullable)
|
|
518
|
+
|
|
519
|
+
def package_name(self, api: RenderableApi) -> str:
|
|
520
|
+
return f'{self.kebab_name(name=api.serviceName)}-api'
|
|
521
|
+
|
|
522
|
+
def client_class_name(self, api: RenderableApi) -> str:
|
|
523
|
+
return f'{self.pascal_name(name=api.serviceName)}ApiV{api.version.major}Client'
|
|
524
|
+
|
|
525
|
+
def operation_name(self, operation: RenderableOperation) -> str:
|
|
526
|
+
return self._operation_name(operation=operation).camelCaseString
|
|
527
|
+
|
|
528
|
+
def path_parameter_segment(self, parameterName: str, isInteger: bool) -> str:
|
|
529
|
+
return f'${{{parameterName}}}' if isInteger else f'${{encodeURIComponent({parameterName})}}'
|
|
530
|
+
|
|
531
|
+
def is_valid_name(self, name: str) -> bool:
|
|
532
|
+
return re.fullmatch(r'[A-Za-z_$][A-Za-z0-9_$]*', name) is not None and name not in self.RESERVED_WORDS
|
|
533
|
+
|
|
534
|
+
def client_operations(self, api: RenderableApi) -> list[RenderableOperation]:
|
|
535
|
+
# NOTE(krishan711): redirects are opened by browsers and query-parameters fields receive third-party callbacks, so neither is called from a client
|
|
536
|
+
return [operation for operation in api.operations if not self.is_redirect(operation=operation) and all(parameter.source == FieldSources.REQUEST for parameter in operation.parameters)]
|
|
537
|
+
|
|
538
|
+
def client_path(self, operation: RenderableOperation) -> str:
|
|
539
|
+
return self.endpoint(operation=operation).removeprefix('/')
|
|
540
|
+
|
|
541
|
+
def body_parameters(self, operation: RenderableOperation) -> list[RenderableParameter]:
|
|
542
|
+
urlParameterNames = {self.identifier(name=parameter.name) for parameter in operation.urlParameters}
|
|
543
|
+
return [parameter for parameter in self.request_parameters(operation=operation) if self.identifier(name=parameter.name) not in urlParameterNames]
|
|
544
|
+
|
|
545
|
+
def is_query_operation(self, operation: RenderableOperation) -> bool:
|
|
546
|
+
return self.http_method(operation=operation) in self.QUERY_METHODS
|
|
547
|
+
|
|
548
|
+
def are_parameters_optional(self, operation: RenderableOperation) -> bool:
|
|
549
|
+
return all(not self.is_parameter_required(operation=operation, parameter=parameter) for parameter in self.body_parameters(operation=operation))
|
|
550
|
+
|
|
551
|
+
def is_parameter_required(self, operation: RenderableOperation, parameter: RenderableParameter) -> bool:
|
|
552
|
+
return parameter.isRequired and parameter.defaultValue is None and not (self.is_query_operation(operation=operation) and parameter.fieldType.isNullable)
|
|
553
|
+
|
|
554
|
+
def parameter_field_type(self, operation: RenderableOperation, parameter: RenderableParameter) -> RenderableType:
|
|
555
|
+
# NOTE(krishan711): null can't be sent in a query string, leaving the parameter out has the same meaning
|
|
556
|
+
isNullable = parameter.fieldType.isNullable and parameter.defaultValue is None and not self.is_query_operation(operation=operation)
|
|
557
|
+
return parameter.fieldType.model_copy(update={'isNullable': isNullable})
|
|
558
|
+
|
|
559
|
+
def response_field(self, operation: RenderableOperation) -> RenderableField | None:
|
|
560
|
+
return operation.responseFields[0] if operation.responseFields else None
|
|
561
|
+
|
|
562
|
+
def stream_callback_name(self, operation: RenderableOperation) -> str:
|
|
563
|
+
responseField = self.response_field(operation=operation)
|
|
564
|
+
if responseField is None:
|
|
565
|
+
raise KibaException(message=f'Stream operation {self.endpoint(operation=operation)} has no response field')
|
|
566
|
+
return f'on{self.pascal_name(name=responseField.name)}'
|
|
567
|
+
|
|
568
|
+
def references_resources(self, renderableTypes: list[RenderableType]) -> bool:
|
|
569
|
+
return any(self._contains_raw_type(renderableType=renderableType, rawType=rawType) for renderableType in renderableTypes for rawType in ('class', PrimitiveTypes.JSON))
|
|
570
|
+
|
|
571
|
+
def requires_date_parsing(self, renderableTypes: list[RenderableType]) -> bool:
|
|
572
|
+
return any(self._contains_raw_type(renderableType=renderableType, rawType=rawType) for renderableType in renderableTypes for rawType in (PrimitiveTypes.DATE, PrimitiveTypes.DATETIME))
|
|
573
|
+
|
|
574
|
+
def resource_field_types(self, api: RenderableApi) -> list[RenderableType]:
|
|
575
|
+
return [attribute.fieldType for resource in api.resources for attribute in resource.attributes]
|
|
576
|
+
|
|
577
|
+
def response_field_types(self, api: RenderableApi) -> list[RenderableType]:
|
|
578
|
+
return [field.fieldType for operation in self.client_operations(api=api) for field in operation.responseFields or []]
|
|
579
|
+
|
|
580
|
+
def body_parameter_types(self, api: RenderableApi) -> list[RenderableType]:
|
|
581
|
+
return [parameter.fieldType for operation in self.client_operations(api=api) for parameter in self.body_parameters(operation=operation)]
|
|
582
|
+
|
|
583
|
+
def _contains_raw_type(self, renderableType: RenderableType, rawType: str) -> bool:
|
|
584
|
+
if isinstance(renderableType, MappingRenderableType):
|
|
585
|
+
return self._contains_raw_type(renderableType=renderableType.valueType, rawType=rawType)
|
|
586
|
+
return renderableType.rawType == rawType
|
|
587
|
+
|
|
588
|
+
def from_object(self, renderableType: RenderableType, value: str, apiVersion: Version, isLocalReference: bool = False) -> str:
|
|
589
|
+
if renderableType.isNullable:
|
|
590
|
+
converted = self.from_object(renderableType=renderableType.model_copy(update={'isNullable': False}), value=value, apiVersion=apiVersion, isLocalReference=isLocalReference)
|
|
591
|
+
return f'{value} != null ? {converted} : null'
|
|
592
|
+
if renderableType.isList:
|
|
593
|
+
itemType = renderableType.model_copy(update={'isList': False})
|
|
594
|
+
itemTypeName = self.type(renderableType=itemType, apiVersion=apiVersion, isLocalReference=isLocalReference)
|
|
595
|
+
return f'({value} as unknown[]).map((entry: unknown): {itemTypeName} => {self.from_object(renderableType=itemType, value="entry", apiVersion=apiVersion, isLocalReference=isLocalReference)})'
|
|
596
|
+
if isinstance(renderableType, MappingRenderableType):
|
|
597
|
+
valueTypeName = self.type(renderableType=renderableType.valueType, apiVersion=apiVersion, isLocalReference=isLocalReference)
|
|
598
|
+
convertedEntry = self.from_object(renderableType=renderableType.valueType, value='entry', apiVersion=apiVersion, isLocalReference=isLocalReference)
|
|
599
|
+
return f'Object.fromEntries(Object.entries({value} as RawObject).map(([key, entry]: [string, unknown]): [string, {valueTypeName}] => [key, {convertedEntry}]))'
|
|
600
|
+
if renderableType.rawType == 'class' and renderableType.className is not None:
|
|
601
|
+
return f'{self.type(renderableType=renderableType, apiVersion=apiVersion, isLocalReference=isLocalReference)}.fromObject({value} as RawObject)'
|
|
602
|
+
if renderableType.rawType == PrimitiveTypes.STRING:
|
|
603
|
+
return f'String({value})'
|
|
604
|
+
if renderableType.rawType in (PrimitiveTypes.INTEGER, PrimitiveTypes.LONG_INTEGER, PrimitiveTypes.FLOAT, PrimitiveTypes.DOUBLE):
|
|
605
|
+
return f'Number({value})'
|
|
606
|
+
if renderableType.rawType == PrimitiveTypes.BOOLEAN:
|
|
607
|
+
return f'Boolean({value})'
|
|
608
|
+
if renderableType.rawType in (PrimitiveTypes.DATE, PrimitiveTypes.DATETIME):
|
|
609
|
+
return f'dateFromString(String({value}))'
|
|
610
|
+
if renderableType.rawType == PrimitiveTypes.JSON:
|
|
611
|
+
return f'{value} as {self.type(renderableType=renderableType, apiVersion=apiVersion, isLocalReference=isLocalReference)}'
|
|
612
|
+
raise KibaException(f'Failed to render conversion from object for: {renderableType}')
|
|
613
|
+
|
|
614
|
+
def _is_serialized_as_is(self, renderableType: RenderableType) -> bool:
|
|
615
|
+
if isinstance(renderableType, MappingRenderableType):
|
|
616
|
+
return self._is_serialized_as_is(renderableType=renderableType.valueType)
|
|
617
|
+
return renderableType.rawType not in ('class', PrimitiveTypes.DATE, PrimitiveTypes.DATETIME)
|
|
618
|
+
|
|
619
|
+
def to_object(self, renderableType: RenderableType, value: str, apiVersion: Version, isOptional: bool = False, isLocalReference: bool = False) -> str:
|
|
620
|
+
if self._is_serialized_as_is(renderableType=renderableType):
|
|
621
|
+
return value
|
|
622
|
+
if renderableType.isNullable or isOptional:
|
|
623
|
+
converted = self.to_object(renderableType=renderableType.model_copy(update={'isNullable': False}), value=value, apiVersion=apiVersion, isLocalReference=isLocalReference)
|
|
624
|
+
return f'{value} == null ? {value} : {converted}'
|
|
625
|
+
if renderableType.isList:
|
|
626
|
+
itemType = renderableType.model_copy(update={'isList': False})
|
|
627
|
+
itemTypeName = self.type(renderableType=itemType, apiVersion=apiVersion, isLocalReference=isLocalReference)
|
|
628
|
+
return f'{value}.map((entry: {itemTypeName}): unknown => {self.to_object(renderableType=itemType, value="entry", apiVersion=apiVersion, isLocalReference=isLocalReference)})'
|
|
629
|
+
if isinstance(renderableType, MappingRenderableType):
|
|
630
|
+
valueTypeName = self.type(renderableType=renderableType.valueType, apiVersion=apiVersion, isLocalReference=isLocalReference)
|
|
631
|
+
convertedEntry = self.to_object(renderableType=renderableType.valueType, value='entry', apiVersion=apiVersion, isLocalReference=isLocalReference)
|
|
632
|
+
return f'Object.fromEntries(Object.entries({value}).map(([key, entry]: [string, {valueTypeName}]): [string, unknown] => [key, {convertedEntry}]))'
|
|
633
|
+
if renderableType.rawType == 'class':
|
|
634
|
+
return f'{value}.toObject()'
|
|
635
|
+
if renderableType.rawType == PrimitiveTypes.DATETIME:
|
|
636
|
+
return f'dateToString({value})'
|
|
637
|
+
if renderableType.rawType == PrimitiveTypes.DATE:
|
|
638
|
+
return f"dateToString({value}, 'yyyy-MM-dd')"
|
|
639
|
+
raise KibaException(f'Failed to render conversion to object for: {renderableType}')
|
geniespec-0.1.1.dev5/src/geniespec/templates/typescript-client/{service}/v{version}/client.ts.j2
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{{ autogenerated_notice }}
|
|
2
|
+
import { RestMethod, ServiceClient } from '@kibalabs/core';
|
|
3
|
+
|
|
4
|
+
import * as Endpoints from './endpoints';
|
|
5
|
+
{% if target.references_resources(renderableTypes=target.response_field_types(api=api)) %}
|
|
6
|
+
import * as Resources from './resources';
|
|
7
|
+
{% endif %}
|
|
8
|
+
|
|
9
|
+
export class {{ target.client_class_name(api=api) }} extends ServiceClient {
|
|
10
|
+
{% for operation in target.client_operations(api=api) %}
|
|
11
|
+
{% set className = target.operation_class_name(operation=operation) %}
|
|
12
|
+
{% set bodyParameters = target.body_parameters(operation=operation) %}
|
|
13
|
+
{% set responseField = target.response_field(operation=operation) %}
|
|
14
|
+
{% set arguments = [] %}
|
|
15
|
+
{% for parameter in operation.urlParameters %}{% set _ = arguments.append(target.identifier(name=parameter.name) ~ ': ' ~ target.type(renderableType=parameter.fieldType, apiVersion=api.version)) %}{% endfor %}
|
|
16
|
+
{% if bodyParameters %}{% set _ = arguments.append('params: Endpoints.' ~ className ~ 'Params' ~ (' = {}' if target.are_parameters_optional(operation=operation) and not target.is_streaming(operation=operation) else '')) %}{% endif %}
|
|
17
|
+
{% if target.is_streaming(operation=operation) %}{% set _ = arguments.append(target.stream_callback_name(operation=operation) ~ ': (' ~ target.identifier(name=responseField.name) ~ ': ' ~ target.type(renderableType=responseField.fieldType, apiVersion=api.version) ~ ') => void') %}{% endif %}
|
|
18
|
+
{% set path = target.client_path(operation=operation) %}
|
|
19
|
+
{% set pathLiteral = ('`' ~ path ~ '`') if operation.urlParameters else ("'" ~ path ~ "'") %}
|
|
20
|
+
{% set request = ('new Endpoints.' ~ className ~ 'Request(params)') if bodyParameters else 'undefined' %}
|
|
21
|
+
{% if not loop.first %}
|
|
22
|
+
|
|
23
|
+
{% endif %}
|
|
24
|
+
{% if target.is_streaming(operation=operation) %}
|
|
25
|
+
public {{ target.operation_name(operation=operation) }} = async ({{ arguments | join(', ') }}): Promise<void> => {
|
|
26
|
+
await this.makeStreamingRequest(RestMethod.{{ target.http_method(operation=operation) }}, {{ pathLiteral }}, {{ request }}, Endpoints.{{ className }}Response, (response: Endpoints.{{ className }}Response): void => {
|
|
27
|
+
{{ target.stream_callback_name(operation=operation) }}(response.{{ target.identifier(name=responseField.name) }});
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
{% elif responseField %}
|
|
31
|
+
public {{ target.operation_name(operation=operation) }} = async ({{ arguments | join(', ') }}): Promise<{{ target.type(renderableType=responseField.fieldType, apiVersion=api.version) }}> => {
|
|
32
|
+
const response = await this.makeRequest(RestMethod.{{ target.http_method(operation=operation) }}, {{ pathLiteral }}, {{ request }}, Endpoints.{{ className }}Response);
|
|
33
|
+
return response.{{ target.identifier(name=responseField.name) }};
|
|
34
|
+
};
|
|
35
|
+
{% else %}
|
|
36
|
+
public {{ target.operation_name(operation=operation) }} = async ({{ arguments | join(', ') }}): Promise<void> => {
|
|
37
|
+
await this.makeRequest(RestMethod.{{ target.http_method(operation=operation) }}, {{ pathLiteral }}, {{ request }});
|
|
38
|
+
};
|
|
39
|
+
{% endif %}
|
|
40
|
+
{% endfor %}
|
|
41
|
+
}
|
geniespec-0.1.1.dev5/src/geniespec/templates/typescript-client/{service}/v{version}/endpoints.ts.j2
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{{ autogenerated_notice }}
|
|
2
|
+
{% set responseTypes = target.response_field_types(api=api) %}
|
|
3
|
+
{% set bodyTypes = target.body_parameter_types(api=api) %}
|
|
4
|
+
{% set coreImports = [] %}
|
|
5
|
+
{% if target.requires_date_parsing(renderableTypes=responseTypes) %}{% set _ = coreImports.append('dateFromString') %}{% endif %}
|
|
6
|
+
{% if target.requires_date_parsing(renderableTypes=bodyTypes) %}{% set _ = coreImports.append('dateToString') %}{% endif %}
|
|
7
|
+
{% set _ = coreImports.append('RawObject') %}
|
|
8
|
+
{% if bodyTypes %}{% set _ = coreImports.append('RequestData') %}{% endif %}
|
|
9
|
+
{% if responseTypes %}{% set _ = coreImports.append('ResponseData') %}{% endif %}
|
|
10
|
+
import { {{ coreImports | join(', ') }} } from '@kibalabs/core';
|
|
11
|
+
{% if target.references_resources(renderableTypes=responseTypes + bodyTypes) %}
|
|
12
|
+
|
|
13
|
+
import * as Resources from './resources';
|
|
14
|
+
{% endif %}
|
|
15
|
+
{% for operation in target.client_operations(api=api) %}
|
|
16
|
+
{% set className = target.operation_class_name(operation=operation) %}
|
|
17
|
+
{% set bodyParameters = target.body_parameters(operation=operation) %}
|
|
18
|
+
{% set responseField = target.response_field(operation=operation) %}
|
|
19
|
+
{% if bodyParameters %}
|
|
20
|
+
|
|
21
|
+
export interface {{ className }}Params {
|
|
22
|
+
{% for parameter in bodyParameters %}
|
|
23
|
+
{{ target.identifier(name=parameter.name) }}{% if not target.is_parameter_required(operation=operation, parameter=parameter) %}?{% endif %}: {{ target.type(renderableType=target.parameter_field_type(operation=operation, parameter=parameter), apiVersion=api.version) }};
|
|
24
|
+
{% endfor %}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class {{ className }}Request extends RequestData {
|
|
28
|
+
public constructor(
|
|
29
|
+
readonly params: {{ className }}Params,
|
|
30
|
+
) {
|
|
31
|
+
super();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public toObject = (): RawObject => {
|
|
35
|
+
return {
|
|
36
|
+
{% for parameter in bodyParameters %}
|
|
37
|
+
{{ target.identifier(name=parameter.name) }}: {{ target.to_object(renderableType=target.parameter_field_type(operation=operation, parameter=parameter), value='this.params.' ~ target.identifier(name=parameter.name), apiVersion=api.version, isOptional=not target.is_parameter_required(operation=operation, parameter=parameter)) }},
|
|
38
|
+
{% endfor %}
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
{% endif %}
|
|
43
|
+
{% if responseField %}
|
|
44
|
+
|
|
45
|
+
export class {{ className }}Response extends ResponseData {
|
|
46
|
+
public constructor(
|
|
47
|
+
readonly {{ target.identifier(name=responseField.name) }}: {{ target.type(renderableType=responseField.fieldType, apiVersion=api.version) }},
|
|
48
|
+
) {
|
|
49
|
+
super();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public static fromObject = (obj: RawObject): {{ className }}Response => {
|
|
53
|
+
return new {{ className }}Response(
|
|
54
|
+
{{ target.from_object(renderableType=responseField.fieldType, value='obj.' ~ target.identifier(name=responseField.name), apiVersion=api.version) }},
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
{% endif %}
|
|
59
|
+
{% endfor %}
|
geniespec-0.1.1.dev5/src/geniespec/templates/typescript-client/{service}/v{version}/resources.ts.j2
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{{ autogenerated_notice }}
|
|
2
|
+
{% if target.requires_date_parsing(renderableTypes=target.resource_field_types(api=api)) %}
|
|
3
|
+
import { dateFromString, dateToString, RawObject } from '@kibalabs/core';
|
|
4
|
+
{% else %}
|
|
5
|
+
import { RawObject } from '@kibalabs/core';
|
|
6
|
+
{% endif %}
|
|
7
|
+
|
|
8
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
|
9
|
+
{% for resource in api.resources %}
|
|
10
|
+
{% set className = target.resource_class_name(resource=resource, apiVersion=api.version) %}
|
|
11
|
+
|
|
12
|
+
export class {{ className }} {
|
|
13
|
+
{% if resource.attributes %}
|
|
14
|
+
public constructor(
|
|
15
|
+
{% for attribute in resource.attributes %}
|
|
16
|
+
readonly {{ target.identifier(name=attribute.name) }}: {{ target.type(renderableType=attribute.fieldType, apiVersion=api.version, isLocalReference=True) }},
|
|
17
|
+
{% endfor %}
|
|
18
|
+
) { }
|
|
19
|
+
|
|
20
|
+
public static fromObject = (obj: RawObject): {{ className }} => {
|
|
21
|
+
return new {{ className }}(
|
|
22
|
+
{% for attribute in resource.attributes %}
|
|
23
|
+
{{ target.from_object(renderableType=attribute.fieldType, value='obj.' ~ target.identifier(name=attribute.name), apiVersion=api.version, isLocalReference=True) }},
|
|
24
|
+
{% endfor %}
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
public toObject = (): RawObject => {
|
|
29
|
+
return {
|
|
30
|
+
{% for attribute in resource.attributes %}
|
|
31
|
+
{{ target.identifier(name=attribute.name) }}: {{ target.to_object(renderableType=attribute.fieldType, value='this.' ~ target.identifier(name=attribute.name), apiVersion=api.version, isLocalReference=True) }},
|
|
32
|
+
{% endfor %}
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
{% else %}
|
|
36
|
+
public static fromObject = (): {{ className }} => {
|
|
37
|
+
return new {{ className }}();
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
public toObject = (): RawObject => {
|
|
41
|
+
return {};
|
|
42
|
+
};
|
|
43
|
+
{% endif %}
|
|
44
|
+
}
|
|
45
|
+
{% endfor %}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|