geniespec 0.1.1.dev5__tar.gz → 0.1.1.dev7__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.dev5 → geniespec-0.1.1.dev7}/PKG-INFO +34 -4
- {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/README.md +33 -3
- {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/pyproject.toml +2 -2
- {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/pyproject.toml.orig +2 -2
- {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/api_schema.yaml +29 -0
- {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/genie.py +70 -19
- {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/languages.py +76 -13
- {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/renderable_types.py +13 -0
- {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/templates/python-server/{service}/v{version}/endpoints.py.j2 +4 -1
- {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/templates/python-server/{service}/v{version}/internal.py.j2 +6 -0
- {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/templates/python-server/{service}/v{version}/resources.py.j2 +9 -0
- {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/templates/typescript-client/{service}/v{version}/client.ts.j2 +8 -1
- {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/templates/typescript-client/{service}/v{version}/endpoints.ts.j2 +25 -1
- {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/templates/typescript-client/{service}/v{version}/resources.ts.j2 +4 -0
- {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/types.py +16 -0
- {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/__init__.py +0 -0
- {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/cli.py +0 -0
- {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/templates/python-server/{service}/__init__.py.j2 +0 -0
- {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/templates/python-server/{service}/v{version}/__init__.py.j2 +0 -0
- {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/templates/python-server/{service}/v{version}/api.py.j2 +0 -0
- {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/templates/python-server/{service}/v{version}/exceptions.py.j2 +0 -0
- {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/templates/typescript-client/{service}/v{version}/index.ts.j2 +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.dev7
|
|
4
4
|
Summary: Generate API code from an api.yaml spec
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Dist: jinja2>=3.1.6
|
|
@@ -22,11 +22,34 @@ geniespec watch --spec api.yaml --target <python-server | typescript-client> [--
|
|
|
22
22
|
```
|
|
23
23
|
|
|
24
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
|
+
- `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`; 0.5.3.dev87 or later for `File` fields), `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 `File` fields send a multipart form. 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`.
|
|
27
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`.
|
|
28
28
|
- The bundled templates are examples of a kibalabs-style target.
|
|
29
29
|
|
|
30
|
+
## Enums and files
|
|
31
|
+
|
|
32
|
+
Enums are string values limited to a list. Name them at the top level to reuse them, or give a field its own:
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
enums:
|
|
36
|
+
- name: ViewKind
|
|
37
|
+
values: [member, cycle-start]
|
|
38
|
+
resources:
|
|
39
|
+
- singleName: View
|
|
40
|
+
collectionName: Views
|
|
41
|
+
attributes:
|
|
42
|
+
- name: kind
|
|
43
|
+
type: ViewKind
|
|
44
|
+
- name: size
|
|
45
|
+
type: Enum
|
|
46
|
+
enumValues: [small, large]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`python-server` types them as `typing.Literal[...]` (named ones as `ViewKindV1` in `resources`) and rejects requests with other values; `typescript-client` types them as `'member' | 'cycle-start'` (named ones as an exported `ViewKindV1`).
|
|
50
|
+
|
|
51
|
+
`type: File` request fields upload a file. They are only allowed on POST transitions, and the transition's other fields must be single primitive or enum values, as the request is sent as a multipart form. Internal methods receive kiba-core's `UploadedFile` (`filename`, `contentType`, `content`); clients pass a `File`.
|
|
52
|
+
|
|
30
53
|
# Development
|
|
31
54
|
|
|
32
55
|
```
|
|
@@ -34,13 +57,20 @@ make install
|
|
|
34
57
|
make lint-check type-check test
|
|
35
58
|
```
|
|
36
59
|
|
|
60
|
+
`tests/python_server` covers the core and the `python-server` target (the core tests generate through it) and runs in `Dockerfile`. `tests/typescript_client` covers the `typescript-client` target; it needs node and runs in `typescript-client.Dockerfile`:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
make install-typescript-client
|
|
64
|
+
make test-typescript-client
|
|
65
|
+
```
|
|
66
|
+
|
|
37
67
|
Merges to `main` publish a dev version to PyPI. To release, bump `version` in `pyproject.toml` in a PR, then run the Release workflow on `main` (Actions → Release → Run workflow). It tags `vX.Y.Z` and the Publish workflow publishes it. Only the release workflow can push version tags.
|
|
38
68
|
|
|
39
69
|
# Things that need work
|
|
40
70
|
|
|
41
71
|
## Typing of params and fields in general
|
|
42
72
|
|
|
43
|
-
|
|
73
|
+
OneOf not supported yet
|
|
44
74
|
|
|
45
75
|
I think we need to create an object for Type. what would it look like?
|
|
46
76
|
|
|
@@ -8,11 +8,34 @@ geniespec watch --spec api.yaml --target <python-server | typescript-client> [--
|
|
|
8
8
|
```
|
|
9
9
|
|
|
10
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
|
+
- `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`; 0.5.3.dev87 or later for `File` fields), `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 `File` fields send a multipart form. 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`.
|
|
13
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`.
|
|
14
14
|
- The bundled templates are examples of a kibalabs-style target.
|
|
15
15
|
|
|
16
|
+
## Enums and files
|
|
17
|
+
|
|
18
|
+
Enums are string values limited to a list. Name them at the top level to reuse them, or give a field its own:
|
|
19
|
+
|
|
20
|
+
```yaml
|
|
21
|
+
enums:
|
|
22
|
+
- name: ViewKind
|
|
23
|
+
values: [member, cycle-start]
|
|
24
|
+
resources:
|
|
25
|
+
- singleName: View
|
|
26
|
+
collectionName: Views
|
|
27
|
+
attributes:
|
|
28
|
+
- name: kind
|
|
29
|
+
type: ViewKind
|
|
30
|
+
- name: size
|
|
31
|
+
type: Enum
|
|
32
|
+
enumValues: [small, large]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`python-server` types them as `typing.Literal[...]` (named ones as `ViewKindV1` in `resources`) and rejects requests with other values; `typescript-client` types them as `'member' | 'cycle-start'` (named ones as an exported `ViewKindV1`).
|
|
36
|
+
|
|
37
|
+
`type: File` request fields upload a file. They are only allowed on POST transitions, and the transition's other fields must be single primitive or enum values, as the request is sent as a multipart form. Internal methods receive kiba-core's `UploadedFile` (`filename`, `contentType`, `content`); clients pass a `File`.
|
|
38
|
+
|
|
16
39
|
# Development
|
|
17
40
|
|
|
18
41
|
```
|
|
@@ -20,13 +43,20 @@ make install
|
|
|
20
43
|
make lint-check type-check test
|
|
21
44
|
```
|
|
22
45
|
|
|
46
|
+
`tests/python_server` covers the core and the `python-server` target (the core tests generate through it) and runs in `Dockerfile`. `tests/typescript_client` covers the `typescript-client` target; it needs node and runs in `typescript-client.Dockerfile`:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
make install-typescript-client
|
|
50
|
+
make test-typescript-client
|
|
51
|
+
```
|
|
52
|
+
|
|
23
53
|
Merges to `main` publish a dev version to PyPI. To release, bump `version` in `pyproject.toml` in a PR, then run the Release workflow on `main` (Actions → Release → Run workflow). It tags `vX.Y.Z` and the Publish workflow publishes it. Only the release workflow can push version tags.
|
|
24
54
|
|
|
25
55
|
# Things that need work
|
|
26
56
|
|
|
27
57
|
## Typing of params and fields in general
|
|
28
58
|
|
|
29
|
-
|
|
59
|
+
OneOf not supported yet
|
|
30
60
|
|
|
31
61
|
I think we need to create an object for Type. what would it look like?
|
|
32
62
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "geniespec"
|
|
3
|
-
version = "0.1.1.
|
|
3
|
+
version = "0.1.1.dev7"
|
|
4
4
|
description = "Generate API code from an api.yaml spec"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -22,7 +22,7 @@ repository = "https://github.com/kibalabs/genie"
|
|
|
22
22
|
[dependency-groups]
|
|
23
23
|
dev = [
|
|
24
24
|
"kiba-build==0.1.11.dev15",
|
|
25
|
-
"kiba-core[core-api,requester]>=0.5.3.
|
|
25
|
+
"kiba-core[core-api,requester]>=0.5.3.dev87,<0.5.4",
|
|
26
26
|
]
|
|
27
27
|
|
|
28
28
|
[build-system]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "geniespec"
|
|
3
|
-
version = "0.1.1.
|
|
3
|
+
version = "0.1.1.dev7"
|
|
4
4
|
description = "Generate API code from an api.yaml spec"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -22,7 +22,7 @@ repository = "https://github.com/kibalabs/genie"
|
|
|
22
22
|
[dependency-groups]
|
|
23
23
|
dev = [
|
|
24
24
|
"kiba-build==0.1.11.dev15",
|
|
25
|
-
"kiba-core[core-api,requester]>=0.5.3.
|
|
25
|
+
"kiba-core[core-api,requester]>=0.5.3.dev87,<0.5.4",
|
|
26
26
|
]
|
|
27
27
|
|
|
28
28
|
[build-system]
|
|
@@ -32,6 +32,10 @@ properties:
|
|
|
32
32
|
type: array
|
|
33
33
|
items:
|
|
34
34
|
$ref: '#/definitions/Exception'
|
|
35
|
+
enums:
|
|
36
|
+
type: array
|
|
37
|
+
items:
|
|
38
|
+
$ref: '#/definitions/Enum'
|
|
35
39
|
imports:
|
|
36
40
|
type: array
|
|
37
41
|
items:
|
|
@@ -42,6 +46,25 @@ properties:
|
|
|
42
46
|
- string
|
|
43
47
|
pattern: '^[a-z][a-z0-9-]*$'
|
|
44
48
|
definitions:
|
|
49
|
+
EnumValues:
|
|
50
|
+
type: array
|
|
51
|
+
minItems: 1
|
|
52
|
+
uniqueItems: true
|
|
53
|
+
items:
|
|
54
|
+
type: string
|
|
55
|
+
pattern: '^[A-Za-z0-9][A-Za-z0-9_-]*$'
|
|
56
|
+
Enum:
|
|
57
|
+
type: object
|
|
58
|
+
additionalProperties: false
|
|
59
|
+
required:
|
|
60
|
+
- name
|
|
61
|
+
- values
|
|
62
|
+
properties:
|
|
63
|
+
name:
|
|
64
|
+
type: string
|
|
65
|
+
pattern: '^[A-Z][A-Za-z]*$'
|
|
66
|
+
values:
|
|
67
|
+
$ref: '#/definitions/EnumValues'
|
|
45
68
|
Attribute:
|
|
46
69
|
type: object
|
|
47
70
|
additionalProperties: false
|
|
@@ -93,6 +116,8 @@ definitions:
|
|
|
93
116
|
items:
|
|
94
117
|
type: string
|
|
95
118
|
pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
|
|
119
|
+
enumValues:
|
|
120
|
+
$ref: '#/definitions/EnumValues'
|
|
96
121
|
Resource:
|
|
97
122
|
type: object
|
|
98
123
|
additionalProperties: false
|
|
@@ -221,6 +246,8 @@ definitions:
|
|
|
221
246
|
items:
|
|
222
247
|
type: string
|
|
223
248
|
pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
|
|
249
|
+
enumValues:
|
|
250
|
+
$ref: '#/definitions/EnumValues'
|
|
224
251
|
isRequired:
|
|
225
252
|
type: boolean
|
|
226
253
|
defaultValue:
|
|
@@ -271,6 +298,8 @@ definitions:
|
|
|
271
298
|
items:
|
|
272
299
|
type: string
|
|
273
300
|
pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
|
|
301
|
+
enumValues:
|
|
302
|
+
$ref: '#/definitions/EnumValues'
|
|
274
303
|
isNullable:
|
|
275
304
|
type: boolean
|
|
276
305
|
isList:
|
|
@@ -24,11 +24,13 @@ from geniespec.renderable_types import ProcessableResource
|
|
|
24
24
|
from geniespec.renderable_types import RenderableApi
|
|
25
25
|
from geniespec.renderable_types import RenderableAttribute
|
|
26
26
|
from geniespec.renderable_types import RenderableEndpoint
|
|
27
|
+
from geniespec.renderable_types import RenderableEnum
|
|
27
28
|
from geniespec.renderable_types import RenderableException
|
|
28
29
|
from geniespec.renderable_types import RenderableField
|
|
29
30
|
from geniespec.renderable_types import RenderableMethod
|
|
30
31
|
from geniespec.renderable_types import RenderableOperation
|
|
31
32
|
from geniespec.renderable_types import RenderableParameter
|
|
33
|
+
from geniespec.renderable_types import RenderableRawTypes
|
|
32
34
|
from geniespec.renderable_types import RenderableResource
|
|
33
35
|
from geniespec.renderable_types import RenderableString
|
|
34
36
|
from geniespec.renderable_types import RenderableType
|
|
@@ -44,10 +46,12 @@ from geniespec.types import RequestField
|
|
|
44
46
|
from geniespec.types import Resource
|
|
45
47
|
from geniespec.types import ResponseField
|
|
46
48
|
from geniespec.types import ResponseTypes
|
|
49
|
+
from geniespec.types import SpecialTypes
|
|
47
50
|
|
|
48
51
|
SCHEMA_FILE_PATH = pathlib.Path(__file__).parent / 'api_schema.yaml'
|
|
49
52
|
TEMPLATES_DIRECTORY_PATH = pathlib.Path(__file__).parent / 'templates'
|
|
50
53
|
TARGET_LANGUAGE_CODES = {'python-server': 'python', 'typescript-client': 'typescript'}
|
|
54
|
+
FORM_FIELD_RAW_TYPES = frozenset({*PrimitiveTypes.ALL, RenderableRawTypes.ENUM, RenderableRawTypes.FILE} - {PrimitiveTypes.JSON})
|
|
51
55
|
|
|
52
56
|
|
|
53
57
|
class SyntacticValidationException(BadRequestException):
|
|
@@ -106,7 +110,7 @@ class Renderer:
|
|
|
106
110
|
method = transition.method or defaultMethods.get(transition.action.upper(), 'POST')
|
|
107
111
|
return RenderableMethod(method=RenderableString.from_upper_case_string(string=method))
|
|
108
112
|
|
|
109
|
-
def _action_operation(self, transition: BaseTransition, endpoint: RenderableEndpoint, knownTypeNames:
|
|
113
|
+
def _action_operation(self, transition: BaseTransition, endpoint: RenderableEndpoint, knownTypeNames: dict[str, str], defaultAuthentication: str | None, isIndirectAction: bool = False) -> RenderableOperation:
|
|
110
114
|
method = self._transition_to_renderable_method(transition=transition)
|
|
111
115
|
fields = [self._request_field_to_renderable_parameter(requestField=requestField, knownTypeNames=knownTypeNames) for requestField in transition.requestFields]
|
|
112
116
|
isQuery = method.method.upperCaseString == 'GET'
|
|
@@ -126,38 +130,50 @@ class Renderer:
|
|
|
126
130
|
rawType: str,
|
|
127
131
|
isNullable: bool,
|
|
128
132
|
isList: bool,
|
|
129
|
-
knownTypeNames:
|
|
133
|
+
knownTypeNames: dict[str, str],
|
|
130
134
|
listValueTypes: list[str] | None = None,
|
|
131
135
|
mappingKeyType: str | None = None,
|
|
132
136
|
mappingValueTypes: list[str] | None = None,
|
|
137
|
+
enumValues: list[str] | None = None,
|
|
133
138
|
) -> RenderableType:
|
|
134
139
|
listValueTypes = listValueTypes or []
|
|
135
140
|
mappingValueTypes = mappingValueTypes or []
|
|
141
|
+
enumValues = enumValues or []
|
|
136
142
|
if rawType == ComplexTypes.MAPPING:
|
|
137
143
|
if mappingKeyType is None or len(mappingValueTypes) == 0:
|
|
138
144
|
raise KibaException(message='Mapping types require mappingKeyType and mappingValueTypes')
|
|
145
|
+
if enumValues:
|
|
146
|
+
raise KibaException(message='Mapping types cannot define enumValues, use a named enum as the value type')
|
|
139
147
|
keyType = self._type_to_renderable_type(rawType=mappingKeyType, isNullable=False, isList=False, knownTypeNames=knownTypeNames)
|
|
140
|
-
nonNullValueTypes = [valueType for valueType in mappingValueTypes if valueType !=
|
|
148
|
+
nonNullValueTypes = [valueType for valueType in mappingValueTypes if valueType != SpecialTypes.NULL]
|
|
141
149
|
if len(nonNullValueTypes) != 1 or len(nonNullValueTypes) - len(mappingValueTypes) not in (0, -1):
|
|
142
150
|
raise KibaException(message=f'Mapping types currently support one value type with an optional Null: {mappingValueTypes}')
|
|
143
|
-
valueType = self._type_to_renderable_type(rawType=nonNullValueTypes[0], isNullable=
|
|
151
|
+
valueType = self._type_to_renderable_type(rawType=nonNullValueTypes[0], isNullable=SpecialTypes.NULL in mappingValueTypes, isList=False, knownTypeNames=knownTypeNames)
|
|
144
152
|
return MappingRenderableType(isNullable=isNullable, isList=isList, keyType=keyType, valueType=valueType)
|
|
145
|
-
if rawType ==
|
|
153
|
+
if rawType == SpecialTypes.LIST:
|
|
146
154
|
if isList or len(listValueTypes) != 1:
|
|
147
155
|
raise KibaException(message='List types require exactly one listValueTypes entry')
|
|
148
|
-
itemType = self._type_to_renderable_type(rawType=listValueTypes[0], isNullable=False, isList=False, knownTypeNames=knownTypeNames)
|
|
156
|
+
itemType = self._type_to_renderable_type(rawType=listValueTypes[0], isNullable=False, isList=False, knownTypeNames=knownTypeNames, enumValues=enumValues)
|
|
149
157
|
if itemType.isList:
|
|
150
158
|
raise KibaException(message='Nested list types are not supported')
|
|
151
|
-
return
|
|
159
|
+
return itemType.model_copy(update={'isNullable': isNullable, 'isList': True})
|
|
152
160
|
if listValueTypes:
|
|
153
161
|
raise KibaException(message=f'Only List types may define listValueTypes: {rawType}')
|
|
162
|
+
if rawType == SpecialTypes.ENUM:
|
|
163
|
+
if not enumValues:
|
|
164
|
+
raise KibaException(message='Enum types require enumValues')
|
|
165
|
+
return RenderableType(rawType=RenderableRawTypes.ENUM, isNullable=isNullable, isList=isList, enumValues=enumValues)
|
|
166
|
+
if enumValues:
|
|
167
|
+
raise KibaException(message=f'Only Enum types may define enumValues: {rawType}')
|
|
168
|
+
if rawType == SpecialTypes.FILE:
|
|
169
|
+
return RenderableType(rawType=RenderableRawTypes.FILE, isNullable=isNullable, isList=isList)
|
|
154
170
|
if rawType in PrimitiveTypes.ALL:
|
|
155
171
|
return RenderableType(rawType=rawType, isNullable=isNullable, isList=isList)
|
|
156
172
|
if rawType in knownTypeNames:
|
|
157
|
-
return RenderableType(rawType=
|
|
173
|
+
return RenderableType(rawType=knownTypeNames[rawType], isNullable=isNullable, className=RenderableString.from_pascal_case_string(string=rawType), isList=isList)
|
|
158
174
|
raise KibaException(message=f'Unknown type: {rawType}')
|
|
159
175
|
|
|
160
|
-
def _request_field_to_renderable_parameter(self, requestField: RequestField, knownTypeNames:
|
|
176
|
+
def _request_field_to_renderable_parameter(self, requestField: RequestField, knownTypeNames: dict[str, str]) -> RenderableParameter:
|
|
161
177
|
return RenderableParameter(
|
|
162
178
|
name=RenderableString.from_kebab_case_string(string=requestField.name),
|
|
163
179
|
fieldType=self._type_to_renderable_type(
|
|
@@ -168,6 +184,7 @@ class Renderer:
|
|
|
168
184
|
listValueTypes=requestField.listValueTypes,
|
|
169
185
|
mappingKeyType=requestField.mappingKeyType,
|
|
170
186
|
mappingValueTypes=requestField.mappingValueTypes,
|
|
187
|
+
enumValues=requestField.enumValues,
|
|
171
188
|
),
|
|
172
189
|
defaultValue=requestField.defaultValue,
|
|
173
190
|
maxValue=requestField.maxValue,
|
|
@@ -177,7 +194,7 @@ class Renderer:
|
|
|
177
194
|
source=requestField.source,
|
|
178
195
|
)
|
|
179
196
|
|
|
180
|
-
def _response_field_to_renderable_field(self, responseField: ResponseField, knownTypeNames:
|
|
197
|
+
def _response_field_to_renderable_field(self, responseField: ResponseField, knownTypeNames: dict[str, str]) -> RenderableField:
|
|
181
198
|
return RenderableField(
|
|
182
199
|
name=RenderableString.from_kebab_case_string(string=responseField.name),
|
|
183
200
|
fieldType=self._type_to_renderable_type(
|
|
@@ -188,10 +205,11 @@ class Renderer:
|
|
|
188
205
|
listValueTypes=responseField.listValueTypes,
|
|
189
206
|
mappingKeyType=responseField.mappingKeyType,
|
|
190
207
|
mappingValueTypes=responseField.mappingValueTypes,
|
|
208
|
+
enumValues=responseField.enumValues,
|
|
191
209
|
),
|
|
192
210
|
)
|
|
193
211
|
|
|
194
|
-
def _attribute_to_renderable_attribute(self, attribute: Attribute, knownTypeNames:
|
|
212
|
+
def _attribute_to_renderable_attribute(self, attribute: Attribute, knownTypeNames: dict[str, str], isNullable: bool | None = None) -> RenderableAttribute:
|
|
195
213
|
return RenderableAttribute(
|
|
196
214
|
name=RenderableString.from_kebab_case_string(string=attribute.name),
|
|
197
215
|
fieldType=self._type_to_renderable_type(
|
|
@@ -202,6 +220,7 @@ class Renderer:
|
|
|
202
220
|
listValueTypes=attribute.listValueTypes,
|
|
203
221
|
mappingKeyType=attribute.mappingKeyType,
|
|
204
222
|
mappingValueTypes=attribute.mappingValueTypes,
|
|
223
|
+
enumValues=attribute.enumValues,
|
|
205
224
|
),
|
|
206
225
|
defaultValue=attribute.defaultValue,
|
|
207
226
|
isUnique=attribute.isUnique,
|
|
@@ -212,11 +231,18 @@ class Renderer:
|
|
|
212
231
|
isNullable=attribute.isNullable,
|
|
213
232
|
)
|
|
214
233
|
|
|
215
|
-
def _attribute_to_renderable_parameter(self, attribute: Attribute, isRequired: bool, knownTypeNames:
|
|
234
|
+
def _attribute_to_renderable_parameter(self, attribute: Attribute, isRequired: bool, knownTypeNames: dict[str, str], shouldUseDefault: bool = True) -> RenderableParameter:
|
|
216
235
|
return RenderableParameter(
|
|
217
236
|
name=RenderableString.from_kebab_case_string(string=attribute.name),
|
|
218
237
|
fieldType=self._type_to_renderable_type(
|
|
219
|
-
rawType=attribute.fieldType,
|
|
238
|
+
rawType=attribute.fieldType,
|
|
239
|
+
isNullable=attribute.isNullable,
|
|
240
|
+
isList=False,
|
|
241
|
+
knownTypeNames=knownTypeNames,
|
|
242
|
+
listValueTypes=attribute.listValueTypes,
|
|
243
|
+
mappingKeyType=attribute.mappingKeyType,
|
|
244
|
+
mappingValueTypes=attribute.mappingValueTypes,
|
|
245
|
+
enumValues=attribute.enumValues,
|
|
220
246
|
),
|
|
221
247
|
defaultValue=attribute.defaultValue if shouldUseDefault else None,
|
|
222
248
|
isRequired=isRequired,
|
|
@@ -227,7 +253,7 @@ class Renderer:
|
|
|
227
253
|
def _resource_full_name(resource: Resource, name: str) -> str:
|
|
228
254
|
return f'{resource.parentResource or ""}{name}'
|
|
229
255
|
|
|
230
|
-
def _resource_to_renderable_resource(self, resource: Resource, knownTypeNames:
|
|
256
|
+
def _resource_to_renderable_resource(self, resource: Resource, knownTypeNames: dict[str, str]) -> RenderableResource:
|
|
231
257
|
attributes = [self._attribute_to_renderable_attribute(attribute=attribute, knownTypeNames=knownTypeNames) for attribute in resource.attributes]
|
|
232
258
|
return RenderableResource(
|
|
233
259
|
singleName=RenderableString.from_pascal_case_string(string=self._resource_full_name(resource=resource, name=resource.singleName)),
|
|
@@ -239,7 +265,7 @@ class Renderer:
|
|
|
239
265
|
accessibleFrom=resource.accessibleFrom,
|
|
240
266
|
)
|
|
241
267
|
|
|
242
|
-
def _get_processable_resource(self, renderableResource: RenderableResource, allResources: list[RenderableResource], knownTypeNames:
|
|
268
|
+
def _get_processable_resource(self, renderableResource: RenderableResource, allResources: list[RenderableResource], knownTypeNames: dict[str, str]) -> ProcessableResource:
|
|
243
269
|
parent: ProcessableResource.Parent | None = None
|
|
244
270
|
attributeOccurrences: list[ProcessableResource.IndependentAttributeOccurrence] = []
|
|
245
271
|
parentResource = next(iter([otherResource for otherResource in allResources if renderableResource.parentResource and otherResource.singleName.pascalCaseString == renderableResource.parentResource]), None)
|
|
@@ -249,7 +275,7 @@ class Renderer:
|
|
|
249
275
|
parent = ProcessableResource.Parent(processableResource=parentProcessableResource, isOneToOne=isOneToOne)
|
|
250
276
|
for otherResource in allResources:
|
|
251
277
|
for attribute in otherResource.independentAttributes:
|
|
252
|
-
if attribute.fieldType.rawType ==
|
|
278
|
+
if attribute.fieldType.rawType == RenderableRawTypes.CLASS and attribute.fieldType.className == renderableResource.singleName:
|
|
253
279
|
occursInResource = self._get_processable_resource(renderableResource=otherResource, allResources=allResources, knownTypeNames=knownTypeNames)
|
|
254
280
|
occursInAttribute = attribute
|
|
255
281
|
attributeOccurrences.append(ProcessableResource.IndependentAttributeOccurrence(processableResource=occursInResource, attribute=occursInAttribute))
|
|
@@ -328,13 +354,36 @@ class Renderer:
|
|
|
328
354
|
errors.append(f'{name}: stream transitions need a responseField')
|
|
329
355
|
if operation.responseType == ResponseTypes.REDIRECT and operation.responseFields:
|
|
330
356
|
errors.append(f'{name}: redirect transitions cannot have a responseField')
|
|
357
|
+
errors += [f'{name}: response field {field.name.kebabCaseString} cannot be a File' for field in operation.responseFields or [] if field.fieldType.rawType == RenderableRawTypes.FILE]
|
|
358
|
+
if any(parameter.fieldType.rawType == RenderableRawTypes.FILE for parameter in operation.parameters):
|
|
359
|
+
if operation.method.method.upperCaseString != 'POST':
|
|
360
|
+
errors.append(f'{name}: only POST transitions can have File fields')
|
|
361
|
+
if operation.responseType == ResponseTypes.STREAM:
|
|
362
|
+
errors.append(f'{name}: stream transitions cannot have File fields')
|
|
363
|
+
errors += [
|
|
364
|
+
f'{name}: field {parameter.name.kebabCaseString} cannot be sent with File fields, only single primitive, enum and File values can'
|
|
365
|
+
for parameter in operation.parameters
|
|
366
|
+
if parameter.source == FieldSources.REQUEST and (parameter.fieldType.isList or isinstance(parameter.fieldType, MappingRenderableType) or parameter.fieldType.rawType not in FORM_FIELD_RAW_TYPES)
|
|
367
|
+
]
|
|
368
|
+
return errors
|
|
369
|
+
|
|
370
|
+
@staticmethod
|
|
371
|
+
def _file_errors(resources: list[RenderableResource], exceptions: list[RenderableException]) -> list[str]:
|
|
372
|
+
errors = [
|
|
373
|
+
f'Attribute {attribute.name.kebabCaseString} of {resource.singleName.pascalCaseString} cannot be a File'
|
|
374
|
+
for resource in resources
|
|
375
|
+
for attribute in [*resource.attributes, *resource.independentAttributes]
|
|
376
|
+
if attribute.fieldType.rawType == RenderableRawTypes.FILE
|
|
377
|
+
]
|
|
378
|
+
errors += [f'Field {field.name.snakeCaseString} of {exception.name.pascalCaseString} cannot be a File' for exception in exceptions for field in exception.fields if field.fieldType.rawType == RenderableRawTypes.FILE]
|
|
331
379
|
return errors
|
|
332
380
|
|
|
333
381
|
def build_renderable_api(self, api: KibaApi) -> RenderableApi:
|
|
334
382
|
fullResourceNames = [self._resource_full_name(resource=resource, name=resource.singleName) for resource in api.resources]
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
resourceErrors
|
|
383
|
+
enumNames = [enum.name for enum in api.enums]
|
|
384
|
+
knownTypeNames = {**dict.fromkeys(fullResourceNames, RenderableRawTypes.CLASS), **dict.fromkeys(enumNames, RenderableRawTypes.ENUM)}
|
|
385
|
+
resourceErrors = [f'Duplicate type name: {name}' for name, count in collections.Counter(fullResourceNames + enumNames).items() if count > 1]
|
|
386
|
+
resourceErrors += [f'Unknown parent resource {resource.parentResource} for {resource.singleName}' for resource in api.resources if resource.parentResource is not None and resource.parentResource not in fullResourceNames]
|
|
338
387
|
resourcesByName = dict(zip(fullResourceNames, api.resources, strict=False))
|
|
339
388
|
for resource, fullName in zip(api.resources, fullResourceNames, strict=True):
|
|
340
389
|
ancestorNames: list[str] = []
|
|
@@ -389,6 +438,7 @@ class Renderer:
|
|
|
389
438
|
resources=[],
|
|
390
439
|
operations=renderableOperations,
|
|
391
440
|
exceptions=renderableExceptions,
|
|
441
|
+
enums=[RenderableEnum(name=RenderableString.from_pascal_case_string(string=enum.name), values=enum.values) for enum in api.enums],
|
|
392
442
|
)
|
|
393
443
|
renderableResources: list[RenderableResource] = [self._resource_to_renderable_resource(resource=resource, knownTypeNames=knownTypeNames) for resource in api.resources]
|
|
394
444
|
uncoveredTransitions: list[str] = []
|
|
@@ -466,6 +516,7 @@ class Renderer:
|
|
|
466
516
|
message=f'Transitions with no endpoint: {", ".join(uncoveredTransitions)}. Item transitions need a primary attribute, collection transitions need direct access or a parent resource, and CREATE is not supported on one-to-one child resources.'
|
|
467
517
|
)
|
|
468
518
|
operationErrors = [error for operation in renderableApi.operations for error in self._operation_errors(operation=operation)]
|
|
519
|
+
operationErrors += self._file_errors(resources=renderableResources, exceptions=renderableExceptions)
|
|
469
520
|
if operationErrors:
|
|
470
521
|
raise SemanticValidationException(message='\n'.join(operationErrors))
|
|
471
522
|
renderableApi.resources.extend(renderableResources)
|
|
@@ -9,10 +9,12 @@ from geniespec.renderable_types import AttributeEndpointComponent
|
|
|
9
9
|
from geniespec.renderable_types import MappingRenderableType
|
|
10
10
|
from geniespec.renderable_types import RenderableApi
|
|
11
11
|
from geniespec.renderable_types import RenderableEndpointComponent
|
|
12
|
+
from geniespec.renderable_types import RenderableEnum
|
|
12
13
|
from geniespec.renderable_types import RenderableException
|
|
13
14
|
from geniespec.renderable_types import RenderableField
|
|
14
15
|
from geniespec.renderable_types import RenderableOperation
|
|
15
16
|
from geniespec.renderable_types import RenderableParameter
|
|
17
|
+
from geniespec.renderable_types import RenderableRawTypes
|
|
16
18
|
from geniespec.renderable_types import RenderableResource
|
|
17
19
|
from geniespec.renderable_types import RenderableString
|
|
18
20
|
from geniespec.renderable_types import RenderableType
|
|
@@ -72,7 +74,10 @@ class BaseLanguageDefinition:
|
|
|
72
74
|
def string_type(self, isNullable: bool) -> str:
|
|
73
75
|
raise NotImplementedError
|
|
74
76
|
|
|
75
|
-
def enum_type(self, isNullable: bool,
|
|
77
|
+
def enum_type(self, isNullable: bool, renderableType: RenderableType, apiVersion: Version, isLocalReference: bool) -> str:
|
|
78
|
+
raise NotImplementedError
|
|
79
|
+
|
|
80
|
+
def file_type(self, isNullable: bool) -> str:
|
|
76
81
|
raise NotImplementedError
|
|
77
82
|
|
|
78
83
|
def package_name(self, api: RenderableApi) -> str:
|
|
@@ -118,14 +123,10 @@ class BaseLanguageDefinition:
|
|
|
118
123
|
return self.date_type(isNullable=isNullable)
|
|
119
124
|
if yamlType == PrimitiveTypes.JSON:
|
|
120
125
|
return self.json_type(isNullable=isNullable)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
# return self.enum_type(isNullable=isNullable, values=[RenderableString.from_kebab_case_string(string=value) for value in values])
|
|
126
|
-
# if yamlType.startswith(PrimitiveTypes.ONE_OF):
|
|
127
|
-
# subTypes = self._render_one_of_type_string(oneOfTypeString=yamlType, isNullable=isNullable, apiVersion=apiVersion)
|
|
128
|
-
# return self.one_of_type(isNullable=isNullable, subTypes=subTypes)
|
|
126
|
+
if yamlType == RenderableRawTypes.ENUM:
|
|
127
|
+
return self.enum_type(isNullable=isNullable, renderableType=renderableType, apiVersion=apiVersion, isLocalReference=isLocalReference)
|
|
128
|
+
if yamlType == RenderableRawTypes.FILE:
|
|
129
|
+
return self.file_type(isNullable=isNullable)
|
|
129
130
|
if yamlType == ComplexTypes.MAPPING and isinstance(renderableType, MappingRenderableType):
|
|
130
131
|
keyType = self.render_type(renderableType=renderableType.keyType, apiVersion=apiVersion, isLocalReference=isLocalReference)
|
|
131
132
|
valueType = self.render_type(renderableType=renderableType.valueType, apiVersion=apiVersion, isLocalReference=isLocalReference)
|
|
@@ -171,6 +172,17 @@ class BaseLanguageDefinition:
|
|
|
171
172
|
def exception_class_name(self, exception: RenderableException, apiVersion: Version) -> str:
|
|
172
173
|
return f'{self.pascal_name(name=exception.name)}V{apiVersion.major}'
|
|
173
174
|
|
|
175
|
+
def enum_class_name(self, enum: RenderableEnum, apiVersion: Version) -> str:
|
|
176
|
+
return f'{self.pascal_name(name=enum.name)}V{apiVersion.major}'
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def is_inline_enum(renderableType: RenderableType) -> bool:
|
|
180
|
+
return renderableType.rawType == RenderableRawTypes.ENUM and renderableType.className is None
|
|
181
|
+
|
|
182
|
+
@staticmethod
|
|
183
|
+
def is_file_operation(operation: RenderableOperation) -> bool:
|
|
184
|
+
return any(parameter.fieldType.rawType == RenderableRawTypes.FILE for parameter in operation.parameters)
|
|
185
|
+
|
|
174
186
|
def type(self, renderableType: RenderableType, apiVersion: Version, isLocalReference: bool = False) -> str:
|
|
175
187
|
return self.render_type(renderableType=renderableType, apiVersion=apiVersion, isLocalReference=isLocalReference)
|
|
176
188
|
|
|
@@ -334,9 +346,19 @@ class PythonLanguageDefinition(BaseLanguageDefinition):
|
|
|
334
346
|
def string_type(self, isNullable: bool) -> str:
|
|
335
347
|
return 'str | None' if isNullable else 'str'
|
|
336
348
|
|
|
337
|
-
def enum_type(self, isNullable: bool,
|
|
338
|
-
|
|
339
|
-
|
|
349
|
+
def enum_type(self, isNullable: bool, renderableType: RenderableType, apiVersion: Version, isLocalReference: bool) -> str:
|
|
350
|
+
if renderableType.className is None:
|
|
351
|
+
typeName = self.enum_literal(values=renderableType.enumValues)
|
|
352
|
+
else:
|
|
353
|
+
typeName = f'{renderableType.className.pascalCaseString}V{apiVersion.major}' if isLocalReference else f'resources.{renderableType.className.pascalCaseString}V{apiVersion.major}'
|
|
354
|
+
return f'{typeName} | None' if isNullable else typeName
|
|
355
|
+
|
|
356
|
+
@staticmethod
|
|
357
|
+
def enum_literal(values: list[str]) -> str:
|
|
358
|
+
return f'typing.Literal[{", ".join(repr(value) for value in values)}]'
|
|
359
|
+
|
|
360
|
+
def file_type(self, isNullable: bool) -> str:
|
|
361
|
+
return 'UploadedFile | None' if isNullable else 'UploadedFile'
|
|
340
362
|
|
|
341
363
|
def package_name(self, api: RenderableApi) -> str:
|
|
342
364
|
return f'{self.snake_name(name=api.serviceName)}_api'
|
|
@@ -393,6 +415,16 @@ class PythonLanguageDefinition(BaseLanguageDefinition):
|
|
|
393
415
|
def resources_require_json(self, api: RenderableApi) -> bool:
|
|
394
416
|
return any(self._contains_json(renderableType=attribute.fieldType) for resource in api.resources for attribute in resource.attributes)
|
|
395
417
|
|
|
418
|
+
def resources_require_typing(self, api: RenderableApi) -> bool:
|
|
419
|
+
return len(api.enums) > 0 or any(self.is_inline_enum(renderableType=attribute.fieldType) for resource in api.resources for attribute in resource.attributes)
|
|
420
|
+
|
|
421
|
+
def operations_require_typing(self, api: RenderableApi) -> bool:
|
|
422
|
+
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 []]
|
|
423
|
+
return any(self.is_inline_enum(renderableType=fieldType) for fieldType in fieldTypes)
|
|
424
|
+
|
|
425
|
+
def requires_uploaded_file(self, api: RenderableApi) -> bool:
|
|
426
|
+
return any(self.is_file_operation(operation=operation) for operation in api.operations)
|
|
427
|
+
|
|
396
428
|
def operations_require_json(self, api: RenderableApi) -> bool:
|
|
397
429
|
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
430
|
return any(self._contains_json(renderableType=fieldType) for fieldType in fieldTypes)
|
|
@@ -516,6 +548,19 @@ class TypeScriptLanguageDefinition(BaseLanguageDefinition):
|
|
|
516
548
|
def string_type(self, isNullable: bool) -> str:
|
|
517
549
|
return self._nullable(typeName='string', isNullable=isNullable)
|
|
518
550
|
|
|
551
|
+
def enum_type(self, isNullable: bool, renderableType: RenderableType, apiVersion: Version, isLocalReference: bool) -> str:
|
|
552
|
+
if renderableType.className is None:
|
|
553
|
+
return self._nullable(typeName=f'({self.enum_union(values=renderableType.enumValues)})', isNullable=isNullable)
|
|
554
|
+
typeName = f'{renderableType.className.pascalCaseString}V{apiVersion.major}'
|
|
555
|
+
return self._nullable(typeName=typeName if isLocalReference else f'Resources.{typeName}', isNullable=isNullable)
|
|
556
|
+
|
|
557
|
+
@staticmethod
|
|
558
|
+
def enum_union(values: list[str]) -> str:
|
|
559
|
+
return ' | '.join(f"'{value}'" for value in values)
|
|
560
|
+
|
|
561
|
+
def file_type(self, isNullable: bool) -> str:
|
|
562
|
+
return self._nullable(typeName='File', isNullable=isNullable)
|
|
563
|
+
|
|
519
564
|
def package_name(self, api: RenderableApi) -> str:
|
|
520
565
|
return f'{self.kebab_name(name=api.serviceName)}-api'
|
|
521
566
|
|
|
@@ -566,7 +611,11 @@ class TypeScriptLanguageDefinition(BaseLanguageDefinition):
|
|
|
566
611
|
return f'on{self.pascal_name(name=responseField.name)}'
|
|
567
612
|
|
|
568
613
|
def references_resources(self, renderableTypes: list[RenderableType]) -> bool:
|
|
569
|
-
|
|
614
|
+
valueTypes = [self._value_type(renderableType=renderableType) for renderableType in renderableTypes]
|
|
615
|
+
return any(valueType.rawType in ('class', PrimitiveTypes.JSON) or (valueType.rawType == RenderableRawTypes.ENUM and valueType.className is not None) for valueType in valueTypes)
|
|
616
|
+
|
|
617
|
+
def _value_type(self, renderableType: RenderableType) -> RenderableType:
|
|
618
|
+
return self._value_type(renderableType=renderableType.valueType) if isinstance(renderableType, MappingRenderableType) else renderableType
|
|
570
619
|
|
|
571
620
|
def requires_date_parsing(self, renderableTypes: list[RenderableType]) -> bool:
|
|
572
621
|
return any(self._contains_raw_type(renderableType=renderableType, rawType=rawType) for renderableType in renderableTypes for rawType in (PrimitiveTypes.DATE, PrimitiveTypes.DATETIME))
|
|
@@ -580,6 +629,18 @@ class TypeScriptLanguageDefinition(BaseLanguageDefinition):
|
|
|
580
629
|
def body_parameter_types(self, api: RenderableApi) -> list[RenderableType]:
|
|
581
630
|
return [parameter.fieldType for operation in self.client_operations(api=api) for parameter in self.body_parameters(operation=operation)]
|
|
582
631
|
|
|
632
|
+
def requires_request_data(self, api: RenderableApi) -> bool:
|
|
633
|
+
return any(self.body_parameters(operation=operation) and not self.is_file_operation(operation=operation) for operation in self.client_operations(api=api))
|
|
634
|
+
|
|
635
|
+
def form_value(self, renderableType: RenderableType, value: str) -> str:
|
|
636
|
+
if renderableType.rawType == RenderableRawTypes.FILE:
|
|
637
|
+
return value
|
|
638
|
+
if renderableType.rawType == PrimitiveTypes.DATETIME:
|
|
639
|
+
return f'dateToString({value})'
|
|
640
|
+
if renderableType.rawType == PrimitiveTypes.DATE:
|
|
641
|
+
return f"dateToString({value}, 'yyyy-MM-dd')"
|
|
642
|
+
return f'String({value})'
|
|
643
|
+
|
|
583
644
|
def _contains_raw_type(self, renderableType: RenderableType, rawType: str) -> bool:
|
|
584
645
|
if isinstance(renderableType, MappingRenderableType):
|
|
585
646
|
return self._contains_raw_type(renderableType=renderableType.valueType, rawType=rawType)
|
|
@@ -609,6 +670,8 @@ class TypeScriptLanguageDefinition(BaseLanguageDefinition):
|
|
|
609
670
|
return f'dateFromString(String({value}))'
|
|
610
671
|
if renderableType.rawType == PrimitiveTypes.JSON:
|
|
611
672
|
return f'{value} as {self.type(renderableType=renderableType, apiVersion=apiVersion, isLocalReference=isLocalReference)}'
|
|
673
|
+
if renderableType.rawType == RenderableRawTypes.ENUM:
|
|
674
|
+
return f'String({value}) as {self.type(renderableType=renderableType, apiVersion=apiVersion, isLocalReference=isLocalReference)}'
|
|
612
675
|
raise KibaException(f'Failed to render conversion from object for: {renderableType}')
|
|
613
676
|
|
|
614
677
|
def _is_serialized_as_is(self, renderableType: RenderableType) -> bool:
|
|
@@ -87,11 +87,23 @@ class RenderableString(BaseModel):
|
|
|
87
87
|
return cls(string=string.split(separator))
|
|
88
88
|
|
|
89
89
|
|
|
90
|
+
class RenderableRawTypes:
|
|
91
|
+
CLASS = 'class'
|
|
92
|
+
ENUM = 'enum'
|
|
93
|
+
FILE = 'file'
|
|
94
|
+
|
|
95
|
+
|
|
90
96
|
class RenderableType(BaseModel):
|
|
91
97
|
rawType: str
|
|
92
98
|
isNullable: bool
|
|
93
99
|
isList: bool = False
|
|
94
100
|
className: RenderableString | None = None
|
|
101
|
+
enumValues: list[str] = []
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class RenderableEnum(BaseModel):
|
|
105
|
+
name: RenderableString
|
|
106
|
+
values: list[str]
|
|
95
107
|
|
|
96
108
|
|
|
97
109
|
class MappingRenderableType(RenderableType):
|
|
@@ -225,6 +237,7 @@ class RenderableApi(BaseModel):
|
|
|
225
237
|
resources: list[RenderableResource]
|
|
226
238
|
operations: list[RenderableOperation]
|
|
227
239
|
exceptions: list[RenderableException]
|
|
240
|
+
enums: list[RenderableEnum] = []
|
|
228
241
|
|
|
229
242
|
|
|
230
243
|
class ProcessableResource(BaseModel):
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{{ autogenerated_notice }}
|
|
2
2
|
import collections.abc
|
|
3
3
|
import datetime
|
|
4
|
-
{% if target.requires_optional_field_default(api=api) %}
|
|
4
|
+
{% if target.requires_optional_field_default(api=api) or target.operations_require_typing(api=api) %}
|
|
5
5
|
import typing
|
|
6
6
|
{% endif %}
|
|
7
7
|
|
|
@@ -16,6 +16,9 @@ from pydantic import BaseModel
|
|
|
16
16
|
{% if target.requires_undefined(api=api) %}
|
|
17
17
|
from core.util.typing_util import UNDEFINED, UpdateValue
|
|
18
18
|
{% endif %}
|
|
19
|
+
{% if target.requires_uploaded_file(api=api) %}
|
|
20
|
+
from core.api.uploaded_file import UploadedFile
|
|
21
|
+
{% endif %}
|
|
19
22
|
|
|
20
23
|
from . import resources
|
|
21
24
|
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
import abc
|
|
3
3
|
import datetime
|
|
4
4
|
import collections.abc
|
|
5
|
+
{% if target.operations_require_typing(api=api) %}
|
|
6
|
+
import typing
|
|
7
|
+
{% endif %}
|
|
5
8
|
|
|
6
9
|
{% if target.operations_require_json(api=api) %}
|
|
7
10
|
import pydantic
|
|
@@ -11,6 +14,9 @@ from . import resources
|
|
|
11
14
|
{% if target.requires_undefined(api=api) %}
|
|
12
15
|
from core.util.typing_util import UNDEFINED, UpdateValue
|
|
13
16
|
{% endif %}
|
|
17
|
+
{% if target.requires_uploaded_file(api=api) %}
|
|
18
|
+
from core.api.uploaded_file import UploadedFile
|
|
19
|
+
{% endif %}
|
|
14
20
|
|
|
15
21
|
|
|
16
22
|
class {{ target.internal_class_name(api=api) }}(abc.ABC):
|
|
@@ -3,12 +3,21 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import collections.abc
|
|
5
5
|
import datetime
|
|
6
|
+
{% if target.resources_require_typing(api=api) %}
|
|
7
|
+
import typing
|
|
8
|
+
{% endif %}
|
|
6
9
|
|
|
7
10
|
{% if target.resources_require_json(api=api) %}
|
|
8
11
|
import pydantic
|
|
9
12
|
{% endif %}
|
|
10
13
|
from pydantic import BaseModel
|
|
11
14
|
|
|
15
|
+
{% for enum in api.enums %}
|
|
16
|
+
{{ target.enum_class_name(enum=enum, apiVersion=api.version) }} = {{ target.enum_literal(values=enum.values) }}
|
|
17
|
+
{% if loop.last %}
|
|
18
|
+
|
|
19
|
+
{% endif %}
|
|
20
|
+
{% endfor %}
|
|
12
21
|
{% for resource in api.resources %}
|
|
13
22
|
|
|
14
23
|
class {{ target.resource_class_name(resource=resource, apiVersion=api.version) }}(BaseModel):
|
|
@@ -21,7 +21,14 @@ export class {{ target.client_class_name(api=api) }} extends ServiceClient {
|
|
|
21
21
|
{% if not loop.first %}
|
|
22
22
|
|
|
23
23
|
{% endif %}
|
|
24
|
-
{% if target.
|
|
24
|
+
{% if target.is_file_operation(operation=operation) %}
|
|
25
|
+
public {{ target.operation_name(operation=operation) }} = async ({{ arguments | join(', ') }}): Promise<{{ target.type(renderableType=responseField.fieldType, apiVersion=api.version) if responseField else 'void' }}> => {
|
|
26
|
+
{% if responseField %}const response = {% endif %}await this.requester.makeFormRequest(`${this.baseUrl}/{{ path }}`, {{ request }}.toFormData());
|
|
27
|
+
{% if responseField %}
|
|
28
|
+
return Endpoints.{{ className }}Response.fromObject(JSON.parse(response.content)).{{ target.identifier(name=responseField.name) }};
|
|
29
|
+
{% endif %}
|
|
30
|
+
};
|
|
31
|
+
{% elif target.is_streaming(operation=operation) %}
|
|
25
32
|
public {{ target.operation_name(operation=operation) }} = async ({{ arguments | join(', ') }}): Promise<void> => {
|
|
26
33
|
await this.makeStreamingRequest(RestMethod.{{ target.http_method(operation=operation) }}, {{ pathLiteral }}, {{ request }}, Endpoints.{{ className }}Response, (response: Endpoints.{{ className }}Response): void => {
|
|
27
34
|
{{ target.stream_callback_name(operation=operation) }}(response.{{ target.identifier(name=responseField.name) }});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
{% if target.requires_date_parsing(renderableTypes=responseTypes) %}{% set _ = coreImports.append('dateFromString') %}{% endif %}
|
|
6
6
|
{% if target.requires_date_parsing(renderableTypes=bodyTypes) %}{% set _ = coreImports.append('dateToString') %}{% endif %}
|
|
7
7
|
{% set _ = coreImports.append('RawObject') %}
|
|
8
|
-
{% if
|
|
8
|
+
{% if target.requires_request_data(api=api) %}{% set _ = coreImports.append('RequestData') %}{% endif %}
|
|
9
9
|
{% if responseTypes %}{% set _ = coreImports.append('ResponseData') %}{% endif %}
|
|
10
10
|
import { {{ coreImports | join(', ') }} } from '@kibalabs/core';
|
|
11
11
|
{% if target.references_resources(renderableTypes=responseTypes + bodyTypes) %}
|
|
@@ -24,6 +24,29 @@ export interface {{ className }}Params {
|
|
|
24
24
|
{% endfor %}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
{% if target.is_file_operation(operation=operation) %}
|
|
28
|
+
export class {{ className }}Request {
|
|
29
|
+
public constructor(
|
|
30
|
+
readonly params: {{ className }}Params,
|
|
31
|
+
) { }
|
|
32
|
+
|
|
33
|
+
public toFormData = (): FormData => {
|
|
34
|
+
const formData = new FormData();
|
|
35
|
+
{% for parameter in bodyParameters %}
|
|
36
|
+
{% set parameterName = target.identifier(name=parameter.name) %}
|
|
37
|
+
{% set value = 'this.params.' ~ parameterName %}
|
|
38
|
+
{% if target.is_parameter_required(operation=operation, parameter=parameter) and not target.parameter_field_type(operation=operation, parameter=parameter).isNullable %}
|
|
39
|
+
formData.append('{{ parameterName }}', {{ target.form_value(renderableType=parameter.fieldType, value=value) }});
|
|
40
|
+
{% else %}
|
|
41
|
+
if ({{ value }} != null) {
|
|
42
|
+
formData.append('{{ parameterName }}', {{ target.form_value(renderableType=parameter.fieldType, value=value) }});
|
|
43
|
+
}
|
|
44
|
+
{% endif %}
|
|
45
|
+
{% endfor %}
|
|
46
|
+
return formData;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
{% else %}
|
|
27
50
|
export class {{ className }}Request extends RequestData {
|
|
28
51
|
public constructor(
|
|
29
52
|
readonly params: {{ className }}Params,
|
|
@@ -40,6 +63,7 @@ export class {{ className }}Request extends RequestData {
|
|
|
40
63
|
};
|
|
41
64
|
}
|
|
42
65
|
{% endif %}
|
|
66
|
+
{% endif %}
|
|
43
67
|
{% if responseField %}
|
|
44
68
|
|
|
45
69
|
export class {{ className }}Response extends ResponseData {
|
|
@@ -6,6 +6,10 @@ import { RawObject } from '@kibalabs/core';
|
|
|
6
6
|
{% endif %}
|
|
7
7
|
|
|
8
8
|
export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
|
9
|
+
{% for enum in api.enums %}
|
|
10
|
+
|
|
11
|
+
export type {{ target.enum_class_name(enum=enum, apiVersion=api.version) }} = {{ target.enum_union(values=enum.values) }};
|
|
12
|
+
{% endfor %}
|
|
9
13
|
{% for resource in api.resources %}
|
|
10
14
|
{% set className = target.resource_class_name(resource=resource, apiVersion=api.version) %}
|
|
11
15
|
|
|
@@ -35,12 +35,25 @@ class ComplexTypes:
|
|
|
35
35
|
ALL: ClassVar[list[str]] = [MAPPING]
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
class SpecialTypes:
|
|
39
|
+
LIST = 'List'
|
|
40
|
+
ENUM = 'Enum'
|
|
41
|
+
FILE = 'File'
|
|
42
|
+
NULL = 'Null'
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ApiEnum(BaseModel):
|
|
46
|
+
name: str
|
|
47
|
+
values: list[str]
|
|
48
|
+
|
|
49
|
+
|
|
38
50
|
class Attribute(BaseModel):
|
|
39
51
|
name: str
|
|
40
52
|
fieldType: str = pydantic.Field(alias='type')
|
|
41
53
|
listValueTypes: list[str] = []
|
|
42
54
|
mappingKeyType: str | None = None
|
|
43
55
|
mappingValueTypes: list[str] = []
|
|
56
|
+
enumValues: list[str] = []
|
|
44
57
|
isPrimary: bool = False
|
|
45
58
|
isUnique: bool = False
|
|
46
59
|
shouldDisableGetEndpoint: bool = False
|
|
@@ -59,6 +72,7 @@ class ResponseField(BaseModel):
|
|
|
59
72
|
listValueTypes: list[str] = []
|
|
60
73
|
mappingKeyType: str | None = None
|
|
61
74
|
mappingValueTypes: list[str] = []
|
|
75
|
+
enumValues: list[str] = []
|
|
62
76
|
isNullable: bool = False
|
|
63
77
|
isList: bool = False
|
|
64
78
|
|
|
@@ -69,6 +83,7 @@ class RequestField(BaseModel):
|
|
|
69
83
|
listValueTypes: list[str] = []
|
|
70
84
|
mappingKeyType: str | None = None
|
|
71
85
|
mappingValueTypes: list[str] = []
|
|
86
|
+
enumValues: list[str] = []
|
|
72
87
|
isRequired: bool = False
|
|
73
88
|
isDeprecated: bool = False
|
|
74
89
|
defaultValue: str | int | float | bool | None = None
|
|
@@ -139,6 +154,7 @@ class KibaApi(BaseModel):
|
|
|
139
154
|
resources: list[Resource] = []
|
|
140
155
|
transitions: list[RootTransition] = []
|
|
141
156
|
exceptions: list[ApiException] = []
|
|
157
|
+
enums: list[ApiEnum] = []
|
|
142
158
|
defaultAuthentication: str | None = None
|
|
143
159
|
|
|
144
160
|
@property
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|