geniespec 0.1.1.dev6__tar.gz → 0.1.1.dev8__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.
Files changed (22) hide show
  1. {geniespec-0.1.1.dev6 → geniespec-0.1.1.dev8}/PKG-INFO +44 -5
  2. {geniespec-0.1.1.dev6 → geniespec-0.1.1.dev8}/README.md +43 -4
  3. {geniespec-0.1.1.dev6 → geniespec-0.1.1.dev8}/pyproject.toml +2 -2
  4. {geniespec-0.1.1.dev6 → geniespec-0.1.1.dev8}/pyproject.toml.orig +2 -2
  5. {geniespec-0.1.1.dev6 → geniespec-0.1.1.dev8}/src/geniespec/api_schema.yaml +54 -0
  6. {geniespec-0.1.1.dev6 → geniespec-0.1.1.dev8}/src/geniespec/genie.py +106 -19
  7. {geniespec-0.1.1.dev6 → geniespec-0.1.1.dev8}/src/geniespec/languages.py +94 -26
  8. {geniespec-0.1.1.dev6 → geniespec-0.1.1.dev8}/src/geniespec/renderable_types.py +26 -0
  9. {geniespec-0.1.1.dev6 → geniespec-0.1.1.dev8}/src/geniespec/templates/python-server/{service}/v{version}/endpoints.py.j2 +4 -1
  10. {geniespec-0.1.1.dev6 → geniespec-0.1.1.dev8}/src/geniespec/templates/python-server/{service}/v{version}/internal.py.j2 +6 -0
  11. {geniespec-0.1.1.dev6 → geniespec-0.1.1.dev8}/src/geniespec/templates/python-server/{service}/v{version}/resources.py.j2 +16 -1
  12. {geniespec-0.1.1.dev6 → geniespec-0.1.1.dev8}/src/geniespec/templates/typescript-client/{service}/v{version}/client.ts.j2 +8 -1
  13. {geniespec-0.1.1.dev6 → geniespec-0.1.1.dev8}/src/geniespec/templates/typescript-client/{service}/v{version}/endpoints.ts.j2 +25 -1
  14. {geniespec-0.1.1.dev6 → geniespec-0.1.1.dev8}/src/geniespec/templates/typescript-client/{service}/v{version}/resources.ts.j2 +21 -0
  15. {geniespec-0.1.1.dev6 → geniespec-0.1.1.dev8}/src/geniespec/types.py +23 -0
  16. {geniespec-0.1.1.dev6 → geniespec-0.1.1.dev8}/src/geniespec/__init__.py +0 -0
  17. {geniespec-0.1.1.dev6 → geniespec-0.1.1.dev8}/src/geniespec/cli.py +0 -0
  18. {geniespec-0.1.1.dev6 → geniespec-0.1.1.dev8}/src/geniespec/templates/python-server/{service}/__init__.py.j2 +0 -0
  19. {geniespec-0.1.1.dev6 → geniespec-0.1.1.dev8}/src/geniespec/templates/python-server/{service}/v{version}/__init__.py.j2 +0 -0
  20. {geniespec-0.1.1.dev6 → geniespec-0.1.1.dev8}/src/geniespec/templates/python-server/{service}/v{version}/api.py.j2 +0 -0
  21. {geniespec-0.1.1.dev6 → geniespec-0.1.1.dev8}/src/geniespec/templates/python-server/{service}/v{version}/exceptions.py.j2 +0 -0
  22. {geniespec-0.1.1.dev6 → geniespec-0.1.1.dev8}/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.dev6
3
+ Version: 0.1.1.dev8
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,52 @@ 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, oneOfs 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
+
53
+ A oneOf is a value that is one of several resources, told apart by a discriminator attribute. Each member resource needs that attribute as a non-nullable enum with a single value:
54
+
55
+ ```yaml
56
+ oneOfs:
57
+ - name: ViewContent
58
+ discriminator: kind
59
+ types: [ChartContent, TableContent]
60
+ resources:
61
+ - singleName: ChartContent
62
+ collectionName: ChartContents
63
+ attributes:
64
+ - name: kind
65
+ type: Enum
66
+ enumValues: [chart]
67
+ ```
68
+
69
+ Fields use it like any other type (`type: ViewContent`, also inside `List` and `Mapping`). `python-server` generates a `ViewContentV1` pydantic discriminated union in `resources`; `typescript-client` generates a `ViewContentV1` union type and a `viewContentV1FromObject` function that builds the member class named by the discriminator (and throws for unknown values).
70
+
30
71
  # Development
31
72
 
32
73
  ```
@@ -47,8 +88,6 @@ Merges to `main` publish a dev version to PyPI. To release, bump `version` in `p
47
88
 
48
89
  ## Typing of params and fields in general
49
90
 
50
- Enum and OneOf not supported yet
51
-
52
91
  I think we need to create an object for Type. what would it look like?
53
92
 
54
93
 
@@ -8,11 +8,52 @@ 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, oneOfs 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
+
39
+ A oneOf is a value that is one of several resources, told apart by a discriminator attribute. Each member resource needs that attribute as a non-nullable enum with a single value:
40
+
41
+ ```yaml
42
+ oneOfs:
43
+ - name: ViewContent
44
+ discriminator: kind
45
+ types: [ChartContent, TableContent]
46
+ resources:
47
+ - singleName: ChartContent
48
+ collectionName: ChartContents
49
+ attributes:
50
+ - name: kind
51
+ type: Enum
52
+ enumValues: [chart]
53
+ ```
54
+
55
+ Fields use it like any other type (`type: ViewContent`, also inside `List` and `Mapping`). `python-server` generates a `ViewContentV1` pydantic discriminated union in `resources`; `typescript-client` generates a `ViewContentV1` union type and a `viewContentV1FromObject` function that builds the member class named by the discriminator (and throws for unknown values).
56
+
16
57
  # Development
17
58
 
18
59
  ```
@@ -33,8 +74,6 @@ Merges to `main` publish a dev version to PyPI. To release, bump `version` in `p
33
74
 
34
75
  ## Typing of params and fields in general
35
76
 
36
- Enum and OneOf not supported yet
37
-
38
77
  I think we need to create an object for Type. what would it look like?
39
78
 
40
79
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "geniespec"
3
- version = "0.1.1.dev6"
3
+ version = "0.1.1.dev8"
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.dev81,<0.5.4",
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.dev6"
3
+ version = "0.1.1.dev8"
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.dev81,<0.5.4",
25
+ "kiba-core[core-api,requester]>=0.5.3.dev87,<0.5.4",
26
26
  ]
27
27
 
28
28
  [build-system]
@@ -32,6 +32,14 @@ properties:
32
32
  type: array
33
33
  items:
34
34
  $ref: '#/definitions/Exception'
35
+ enums:
36
+ type: array
37
+ items:
38
+ $ref: '#/definitions/Enum'
39
+ oneOfs:
40
+ type: array
41
+ items:
42
+ $ref: '#/definitions/OneOf'
35
43
  imports:
36
44
  type: array
37
45
  items:
@@ -42,6 +50,46 @@ properties:
42
50
  - string
43
51
  pattern: '^[a-z][a-z0-9-]*$'
44
52
  definitions:
53
+ EnumValues:
54
+ type: array
55
+ minItems: 1
56
+ uniqueItems: true
57
+ items:
58
+ type: string
59
+ pattern: '^[A-Za-z0-9][A-Za-z0-9_-]*$'
60
+ Enum:
61
+ type: object
62
+ additionalProperties: false
63
+ required:
64
+ - name
65
+ - values
66
+ properties:
67
+ name:
68
+ type: string
69
+ pattern: '^[A-Z][A-Za-z]*$'
70
+ values:
71
+ $ref: '#/definitions/EnumValues'
72
+ OneOf:
73
+ type: object
74
+ additionalProperties: false
75
+ required:
76
+ - name
77
+ - discriminator
78
+ - types
79
+ properties:
80
+ name:
81
+ type: string
82
+ pattern: '^[A-Z][A-Za-z]*$'
83
+ discriminator:
84
+ type: string
85
+ pattern: '^[a-z][A-Za-z0-9-]*$'
86
+ types:
87
+ type: array
88
+ minItems: 2
89
+ uniqueItems: true
90
+ items:
91
+ type: string
92
+ pattern: '^[A-Z][A-Za-z-]*$'
45
93
  Attribute:
46
94
  type: object
47
95
  additionalProperties: false
@@ -93,6 +141,8 @@ definitions:
93
141
  items:
94
142
  type: string
95
143
  pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
144
+ enumValues:
145
+ $ref: '#/definitions/EnumValues'
96
146
  Resource:
97
147
  type: object
98
148
  additionalProperties: false
@@ -221,6 +271,8 @@ definitions:
221
271
  items:
222
272
  type: string
223
273
  pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
274
+ enumValues:
275
+ $ref: '#/definitions/EnumValues'
224
276
  isRequired:
225
277
  type: boolean
226
278
  defaultValue:
@@ -271,6 +323,8 @@ definitions:
271
323
  items:
272
324
  type: string
273
325
  pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
326
+ enumValues:
327
+ $ref: '#/definitions/EnumValues'
274
328
  isNullable:
275
329
  type: boolean
276
330
  isList:
@@ -24,16 +24,22 @@ 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
31
+ from geniespec.renderable_types import RenderableOneOf
32
+ from geniespec.renderable_types import RenderableOneOfMember
30
33
  from geniespec.renderable_types import RenderableOperation
31
34
  from geniespec.renderable_types import RenderableParameter
35
+ from geniespec.renderable_types import RenderableRawTypes
32
36
  from geniespec.renderable_types import RenderableResource
33
37
  from geniespec.renderable_types import RenderableString
34
38
  from geniespec.renderable_types import RenderableType
35
39
  from geniespec.renderable_types import ResourceEndpointComponent
36
40
  from geniespec.renderable_types import SingleResourceEndpointComponent
41
+ from geniespec.types import ApiEnum
42
+ from geniespec.types import ApiOneOf
37
43
  from geniespec.types import Attribute
38
44
  from geniespec.types import BaseTransition
39
45
  from geniespec.types import ComplexTypes
@@ -44,10 +50,12 @@ from geniespec.types import RequestField
44
50
  from geniespec.types import Resource
45
51
  from geniespec.types import ResponseField
46
52
  from geniespec.types import ResponseTypes
53
+ from geniespec.types import SpecialTypes
47
54
 
48
55
  SCHEMA_FILE_PATH = pathlib.Path(__file__).parent / 'api_schema.yaml'
49
56
  TEMPLATES_DIRECTORY_PATH = pathlib.Path(__file__).parent / 'templates'
50
57
  TARGET_LANGUAGE_CODES = {'python-server': 'python', 'typescript-client': 'typescript'}
58
+ FORM_FIELD_RAW_TYPES = frozenset({*PrimitiveTypes.ALL, RenderableRawTypes.ENUM, RenderableRawTypes.FILE} - {PrimitiveTypes.JSON})
51
59
 
52
60
 
53
61
  class SyntacticValidationException(BadRequestException):
@@ -106,7 +114,7 @@ class Renderer:
106
114
  method = transition.method or defaultMethods.get(transition.action.upper(), 'POST')
107
115
  return RenderableMethod(method=RenderableString.from_upper_case_string(string=method))
108
116
 
109
- def _action_operation(self, transition: BaseTransition, endpoint: RenderableEndpoint, knownTypeNames: set[str], defaultAuthentication: str | None, isIndirectAction: bool = False) -> RenderableOperation:
117
+ def _action_operation(self, transition: BaseTransition, endpoint: RenderableEndpoint, knownTypeNames: dict[str, str], defaultAuthentication: str | None, isIndirectAction: bool = False) -> RenderableOperation:
110
118
  method = self._transition_to_renderable_method(transition=transition)
111
119
  fields = [self._request_field_to_renderable_parameter(requestField=requestField, knownTypeNames=knownTypeNames) for requestField in transition.requestFields]
112
120
  isQuery = method.method.upperCaseString == 'GET'
@@ -126,38 +134,50 @@ class Renderer:
126
134
  rawType: str,
127
135
  isNullable: bool,
128
136
  isList: bool,
129
- knownTypeNames: set[str],
137
+ knownTypeNames: dict[str, str],
130
138
  listValueTypes: list[str] | None = None,
131
139
  mappingKeyType: str | None = None,
132
140
  mappingValueTypes: list[str] | None = None,
141
+ enumValues: list[str] | None = None,
133
142
  ) -> RenderableType:
134
143
  listValueTypes = listValueTypes or []
135
144
  mappingValueTypes = mappingValueTypes or []
145
+ enumValues = enumValues or []
136
146
  if rawType == ComplexTypes.MAPPING:
137
147
  if mappingKeyType is None or len(mappingValueTypes) == 0:
138
148
  raise KibaException(message='Mapping types require mappingKeyType and mappingValueTypes')
149
+ if enumValues:
150
+ raise KibaException(message='Mapping types cannot define enumValues, use a named enum as the value type')
139
151
  keyType = self._type_to_renderable_type(rawType=mappingKeyType, isNullable=False, isList=False, knownTypeNames=knownTypeNames)
140
- nonNullValueTypes = [valueType for valueType in mappingValueTypes if valueType != 'Null']
152
+ nonNullValueTypes = [valueType for valueType in mappingValueTypes if valueType != SpecialTypes.NULL]
141
153
  if len(nonNullValueTypes) != 1 or len(nonNullValueTypes) - len(mappingValueTypes) not in (0, -1):
142
154
  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='Null' in mappingValueTypes, isList=False, knownTypeNames=knownTypeNames)
155
+ valueType = self._type_to_renderable_type(rawType=nonNullValueTypes[0], isNullable=SpecialTypes.NULL in mappingValueTypes, isList=False, knownTypeNames=knownTypeNames)
144
156
  return MappingRenderableType(isNullable=isNullable, isList=isList, keyType=keyType, valueType=valueType)
145
- if rawType == 'List':
157
+ if rawType == SpecialTypes.LIST:
146
158
  if isList or len(listValueTypes) != 1:
147
159
  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)
160
+ itemType = self._type_to_renderable_type(rawType=listValueTypes[0], isNullable=False, isList=False, knownTypeNames=knownTypeNames, enumValues=enumValues)
149
161
  if itemType.isList:
150
162
  raise KibaException(message='Nested list types are not supported')
151
- return RenderableType(rawType=itemType.rawType, isNullable=isNullable, isList=True, className=itemType.className)
163
+ return itemType.model_copy(update={'isNullable': isNullable, 'isList': True})
152
164
  if listValueTypes:
153
165
  raise KibaException(message=f'Only List types may define listValueTypes: {rawType}')
166
+ if rawType == SpecialTypes.ENUM:
167
+ if not enumValues:
168
+ raise KibaException(message='Enum types require enumValues')
169
+ return RenderableType(rawType=RenderableRawTypes.ENUM, isNullable=isNullable, isList=isList, enumValues=enumValues)
170
+ if enumValues:
171
+ raise KibaException(message=f'Only Enum types may define enumValues: {rawType}')
172
+ if rawType == SpecialTypes.FILE:
173
+ return RenderableType(rawType=RenderableRawTypes.FILE, isNullable=isNullable, isList=isList)
154
174
  if rawType in PrimitiveTypes.ALL:
155
175
  return RenderableType(rawType=rawType, isNullable=isNullable, isList=isList)
156
176
  if rawType in knownTypeNames:
157
- return RenderableType(rawType='class', isNullable=isNullable, className=RenderableString.from_pascal_case_string(string=rawType), isList=isList)
177
+ return RenderableType(rawType=knownTypeNames[rawType], isNullable=isNullable, className=RenderableString.from_pascal_case_string(string=rawType), isList=isList)
158
178
  raise KibaException(message=f'Unknown type: {rawType}')
159
179
 
160
- def _request_field_to_renderable_parameter(self, requestField: RequestField, knownTypeNames: set[str]) -> RenderableParameter:
180
+ def _request_field_to_renderable_parameter(self, requestField: RequestField, knownTypeNames: dict[str, str]) -> RenderableParameter:
161
181
  return RenderableParameter(
162
182
  name=RenderableString.from_kebab_case_string(string=requestField.name),
163
183
  fieldType=self._type_to_renderable_type(
@@ -168,6 +188,7 @@ class Renderer:
168
188
  listValueTypes=requestField.listValueTypes,
169
189
  mappingKeyType=requestField.mappingKeyType,
170
190
  mappingValueTypes=requestField.mappingValueTypes,
191
+ enumValues=requestField.enumValues,
171
192
  ),
172
193
  defaultValue=requestField.defaultValue,
173
194
  maxValue=requestField.maxValue,
@@ -177,7 +198,7 @@ class Renderer:
177
198
  source=requestField.source,
178
199
  )
179
200
 
180
- def _response_field_to_renderable_field(self, responseField: ResponseField, knownTypeNames: set[str]) -> RenderableField:
201
+ def _response_field_to_renderable_field(self, responseField: ResponseField, knownTypeNames: dict[str, str]) -> RenderableField:
181
202
  return RenderableField(
182
203
  name=RenderableString.from_kebab_case_string(string=responseField.name),
183
204
  fieldType=self._type_to_renderable_type(
@@ -188,10 +209,11 @@ class Renderer:
188
209
  listValueTypes=responseField.listValueTypes,
189
210
  mappingKeyType=responseField.mappingKeyType,
190
211
  mappingValueTypes=responseField.mappingValueTypes,
212
+ enumValues=responseField.enumValues,
191
213
  ),
192
214
  )
193
215
 
194
- def _attribute_to_renderable_attribute(self, attribute: Attribute, knownTypeNames: set[str], isNullable: bool | None = None) -> RenderableAttribute:
216
+ def _attribute_to_renderable_attribute(self, attribute: Attribute, knownTypeNames: dict[str, str], isNullable: bool | None = None) -> RenderableAttribute:
195
217
  return RenderableAttribute(
196
218
  name=RenderableString.from_kebab_case_string(string=attribute.name),
197
219
  fieldType=self._type_to_renderable_type(
@@ -202,6 +224,7 @@ class Renderer:
202
224
  listValueTypes=attribute.listValueTypes,
203
225
  mappingKeyType=attribute.mappingKeyType,
204
226
  mappingValueTypes=attribute.mappingValueTypes,
227
+ enumValues=attribute.enumValues,
205
228
  ),
206
229
  defaultValue=attribute.defaultValue,
207
230
  isUnique=attribute.isUnique,
@@ -212,11 +235,18 @@ class Renderer:
212
235
  isNullable=attribute.isNullable,
213
236
  )
214
237
 
215
- def _attribute_to_renderable_parameter(self, attribute: Attribute, isRequired: bool, knownTypeNames: set[str], shouldUseDefault: bool = True) -> RenderableParameter:
238
+ def _attribute_to_renderable_parameter(self, attribute: Attribute, isRequired: bool, knownTypeNames: dict[str, str], shouldUseDefault: bool = True) -> RenderableParameter:
216
239
  return RenderableParameter(
217
240
  name=RenderableString.from_kebab_case_string(string=attribute.name),
218
241
  fieldType=self._type_to_renderable_type(
219
- rawType=attribute.fieldType, isNullable=attribute.isNullable, isList=False, knownTypeNames=knownTypeNames, listValueTypes=attribute.listValueTypes, mappingKeyType=attribute.mappingKeyType, mappingValueTypes=attribute.mappingValueTypes
242
+ rawType=attribute.fieldType,
243
+ isNullable=attribute.isNullable,
244
+ isList=False,
245
+ knownTypeNames=knownTypeNames,
246
+ listValueTypes=attribute.listValueTypes,
247
+ mappingKeyType=attribute.mappingKeyType,
248
+ mappingValueTypes=attribute.mappingValueTypes,
249
+ enumValues=attribute.enumValues,
220
250
  ),
221
251
  defaultValue=attribute.defaultValue if shouldUseDefault else None,
222
252
  isRequired=isRequired,
@@ -227,7 +257,7 @@ class Renderer:
227
257
  def _resource_full_name(resource: Resource, name: str) -> str:
228
258
  return f'{resource.parentResource or ""}{name}'
229
259
 
230
- def _resource_to_renderable_resource(self, resource: Resource, knownTypeNames: set[str]) -> RenderableResource:
260
+ def _resource_to_renderable_resource(self, resource: Resource, knownTypeNames: dict[str, str]) -> RenderableResource:
231
261
  attributes = [self._attribute_to_renderable_attribute(attribute=attribute, knownTypeNames=knownTypeNames) for attribute in resource.attributes]
232
262
  return RenderableResource(
233
263
  singleName=RenderableString.from_pascal_case_string(string=self._resource_full_name(resource=resource, name=resource.singleName)),
@@ -239,7 +269,7 @@ class Renderer:
239
269
  accessibleFrom=resource.accessibleFrom,
240
270
  )
241
271
 
242
- def _get_processable_resource(self, renderableResource: RenderableResource, allResources: list[RenderableResource], knownTypeNames: set[str]) -> ProcessableResource:
272
+ def _get_processable_resource(self, renderableResource: RenderableResource, allResources: list[RenderableResource], knownTypeNames: dict[str, str]) -> ProcessableResource:
243
273
  parent: ProcessableResource.Parent | None = None
244
274
  attributeOccurrences: list[ProcessableResource.IndependentAttributeOccurrence] = []
245
275
  parentResource = next(iter([otherResource for otherResource in allResources if renderableResource.parentResource and otherResource.singleName.pascalCaseString == renderableResource.parentResource]), None)
@@ -249,7 +279,7 @@ class Renderer:
249
279
  parent = ProcessableResource.Parent(processableResource=parentProcessableResource, isOneToOne=isOneToOne)
250
280
  for otherResource in allResources:
251
281
  for attribute in otherResource.independentAttributes:
252
- if attribute.fieldType.rawType == 'class' and attribute.fieldType.className == renderableResource.singleName:
282
+ if attribute.fieldType.rawType == RenderableRawTypes.CLASS and attribute.fieldType.className == renderableResource.singleName:
253
283
  occursInResource = self._get_processable_resource(renderableResource=otherResource, allResources=allResources, knownTypeNames=knownTypeNames)
254
284
  occursInAttribute = attribute
255
285
  attributeOccurrences.append(ProcessableResource.IndependentAttributeOccurrence(processableResource=occursInResource, attribute=occursInAttribute))
@@ -328,14 +358,68 @@ class Renderer:
328
358
  errors.append(f'{name}: stream transitions need a responseField')
329
359
  if operation.responseType == ResponseTypes.REDIRECT and operation.responseFields:
330
360
  errors.append(f'{name}: redirect transitions cannot have a responseField')
361
+ errors += [f'{name}: response field {field.name.kebabCaseString} cannot be a File' for field in operation.responseFields or [] if field.fieldType.rawType == RenderableRawTypes.FILE]
362
+ if any(parameter.fieldType.rawType == RenderableRawTypes.FILE for parameter in operation.parameters):
363
+ if operation.method.method.upperCaseString != 'POST':
364
+ errors.append(f'{name}: only POST transitions can have File fields')
365
+ if operation.responseType == ResponseTypes.STREAM:
366
+ errors.append(f'{name}: stream transitions cannot have File fields')
367
+ errors += [
368
+ f'{name}: field {parameter.name.kebabCaseString} cannot be sent with File fields, only single primitive, enum and File values can'
369
+ for parameter in operation.parameters
370
+ if parameter.source == FieldSources.REQUEST and (parameter.fieldType.isList or isinstance(parameter.fieldType, MappingRenderableType) or parameter.fieldType.rawType not in FORM_FIELD_RAW_TYPES)
371
+ ]
331
372
  return errors
332
373
 
374
+ @staticmethod
375
+ def _file_errors(resources: list[RenderableResource], exceptions: list[RenderableException]) -> list[str]:
376
+ errors = [
377
+ f'Attribute {attribute.name.kebabCaseString} of {resource.singleName.pascalCaseString} cannot be a File'
378
+ for resource in resources
379
+ for attribute in [*resource.attributes, *resource.independentAttributes]
380
+ if attribute.fieldType.rawType == RenderableRawTypes.FILE
381
+ ]
382
+ 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]
383
+ return errors
384
+
385
+ @staticmethod
386
+ def _one_of_members(oneOf: ApiOneOf, resourcesByName: dict[str, Resource], enumsByName: dict[str, ApiEnum]) -> tuple[list[RenderableOneOfMember], list[str]]:
387
+ members: list[RenderableOneOfMember] = []
388
+ errors: list[str] = []
389
+ for typeName in oneOf.types:
390
+ resource = resourcesByName.get(typeName)
391
+ if resource is None:
392
+ errors.append(f'Unknown resource {typeName} in {oneOf.name}')
393
+ continue
394
+ attribute = next((attribute for attribute in resource.attributes if attribute.name == oneOf.discriminator), None)
395
+ if attribute is None:
396
+ errors.append(f'{typeName} needs a {oneOf.discriminator} attribute to be part of {oneOf.name}')
397
+ continue
398
+ values = attribute.enumValues if attribute.fieldType == SpecialTypes.ENUM else enumsByName[attribute.fieldType].values if attribute.fieldType in enumsByName else []
399
+ if len(values) != 1 or attribute.isNullable:
400
+ errors.append(f'{oneOf.discriminator} of {typeName} must be a non-nullable Enum with one value to be part of {oneOf.name}')
401
+ continue
402
+ members.append(RenderableOneOfMember(className=RenderableString.from_pascal_case_string(string=typeName), value=values[0]))
403
+ errors += [f'{oneOf.name} has more than one {oneOf.discriminator} {value}' for value, count in collections.Counter(member.value for member in members).items() if count > 1]
404
+ return members, errors
405
+
333
406
  def build_renderable_api(self, api: KibaApi) -> RenderableApi:
334
407
  fullResourceNames = [self._resource_full_name(resource=resource, name=resource.singleName) for resource in api.resources]
335
- knownTypeNames = set(fullResourceNames)
336
- resourceErrors = [f'Duplicate resource name: {name}' for name, count in collections.Counter(fullResourceNames).items() if count > 1]
337
- 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 knownTypeNames]
408
+ enumNames = [enum.name for enum in api.enums]
409
+ oneOfNames = [oneOf.name for oneOf in api.oneOfs]
410
+ knownTypeNames = {
411
+ **dict.fromkeys(fullResourceNames, RenderableRawTypes.CLASS),
412
+ **dict.fromkeys(enumNames, RenderableRawTypes.ENUM),
413
+ **dict.fromkeys(oneOfNames, RenderableRawTypes.ONE_OF),
414
+ }
415
+ resourceErrors = [f'Duplicate type name: {name}' for name, count in collections.Counter(fullResourceNames + enumNames + oneOfNames).items() if count > 1]
416
+ 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
417
  resourcesByName = dict(zip(fullResourceNames, api.resources, strict=False))
418
+ renderableOneOfs: list[RenderableOneOf] = []
419
+ for oneOf in api.oneOfs:
420
+ members, oneOfErrors = self._one_of_members(oneOf=oneOf, resourcesByName=resourcesByName, enumsByName={enum.name: enum for enum in api.enums})
421
+ resourceErrors += oneOfErrors
422
+ renderableOneOfs.append(RenderableOneOf(name=RenderableString.from_pascal_case_string(string=oneOf.name), discriminator=RenderableString.from_kebab_case_string(string=oneOf.discriminator), members=members))
339
423
  for resource, fullName in zip(api.resources, fullResourceNames, strict=True):
340
424
  ancestorNames: list[str] = []
341
425
  ancestor = resourcesByName.get(resource.parentResource) if resource.parentResource is not None else None
@@ -389,6 +473,8 @@ class Renderer:
389
473
  resources=[],
390
474
  operations=renderableOperations,
391
475
  exceptions=renderableExceptions,
476
+ enums=[RenderableEnum(name=RenderableString.from_pascal_case_string(string=enum.name), values=enum.values) for enum in api.enums],
477
+ oneOfs=renderableOneOfs,
392
478
  )
393
479
  renderableResources: list[RenderableResource] = [self._resource_to_renderable_resource(resource=resource, knownTypeNames=knownTypeNames) for resource in api.resources]
394
480
  uncoveredTransitions: list[str] = []
@@ -466,6 +552,7 @@ class Renderer:
466
552
  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
553
  )
468
554
  operationErrors = [error for operation in renderableApi.operations for error in self._operation_errors(operation=operation)]
555
+ operationErrors += self._file_errors(resources=renderableResources, exceptions=renderableExceptions)
469
556
  if operationErrors:
470
557
  raise SemanticValidationException(message='\n'.join(operationErrors))
471
558
  renderableApi.resources.extend(renderableResources)
@@ -9,10 +9,13 @@ 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
15
+ from geniespec.renderable_types import RenderableOneOf
14
16
  from geniespec.renderable_types import RenderableOperation
15
17
  from geniespec.renderable_types import RenderableParameter
18
+ from geniespec.renderable_types import RenderableRawTypes
16
19
  from geniespec.renderable_types import RenderableResource
17
20
  from geniespec.renderable_types import RenderableString
18
21
  from geniespec.renderable_types import RenderableType
@@ -66,13 +69,13 @@ class BaseLanguageDefinition:
66
69
  def object_type(self, isNullable: bool, className: RenderableString, apiVersion: Version, isLocalReference: bool) -> str:
67
70
  raise NotImplementedError
68
71
 
69
- def one_of_type(self, isNullable: bool, subTypes: list[str]) -> str:
72
+ def string_type(self, isNullable: bool) -> str:
70
73
  raise NotImplementedError
71
74
 
72
- def string_type(self, isNullable: bool) -> str:
75
+ def enum_type(self, isNullable: bool, renderableType: RenderableType, apiVersion: Version, isLocalReference: bool) -> str:
73
76
  raise NotImplementedError
74
77
 
75
- def enum_type(self, isNullable: bool, values: list[str]) -> str:
78
+ def file_type(self, isNullable: bool) -> str:
76
79
  raise NotImplementedError
77
80
 
78
81
  def package_name(self, api: RenderableApi) -> str:
@@ -87,10 +90,6 @@ class BaseLanguageDefinition:
87
90
  def is_valid_name(self, name: str) -> bool:
88
91
  raise NotImplementedError
89
92
 
90
- def _render_one_of_type_string(self, oneOfTypeString: str, isNullable: bool, apiVersion: Version) -> list[str]:
91
- oneOfTypes = ''.join(oneOfTypeString.replace('OneOf[', '', 1).rsplit(']', 1)).split(', ')
92
- return [self.render_type(renderableType=RenderableType(rawType=oneOfType, isNullable=isNullable), apiVersion=apiVersion) for oneOfType in oneOfTypes]
93
-
94
93
  def _render_list_type_string(self, listTypeString: str, apiVersion: Version) -> str:
95
94
  listItemType = ''.join(listTypeString.replace('List[', '', 1).rsplit(']', 1))
96
95
  return self.render_type(renderableType=RenderableType(rawType=listItemType, isNullable=False), apiVersion=apiVersion)
@@ -118,19 +117,15 @@ class BaseLanguageDefinition:
118
117
  return self.date_type(isNullable=isNullable)
119
118
  if yamlType == PrimitiveTypes.JSON:
120
119
  return self.json_type(isNullable=isNullable)
121
- # if yamlType == PrimitiveTypes.STRUCT:
122
- # return self.struct_type(isNullable=isNullable)
123
- # if yamlType.startswith(PrimitiveTypes.ENUM):
124
- # values = ''.join(yamlType.replace('Enum[', '', 1).rsplit(']', 1)).split(', ')
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)
120
+ if yamlType == RenderableRawTypes.ENUM:
121
+ return self.enum_type(isNullable=isNullable, renderableType=renderableType, apiVersion=apiVersion, isLocalReference=isLocalReference)
122
+ if yamlType == RenderableRawTypes.FILE:
123
+ return self.file_type(isNullable=isNullable)
129
124
  if yamlType == ComplexTypes.MAPPING and isinstance(renderableType, MappingRenderableType):
130
125
  keyType = self.render_type(renderableType=renderableType.keyType, apiVersion=apiVersion, isLocalReference=isLocalReference)
131
126
  valueType = self.render_type(renderableType=renderableType.valueType, apiVersion=apiVersion, isLocalReference=isLocalReference)
132
127
  return self.mapping_type(isNullable=isNullable, keyType=keyType, valueType=valueType)
133
- if yamlType == 'class' and isinstance(renderableType, RenderableType) and renderableType.className is not None:
128
+ if yamlType in (RenderableRawTypes.CLASS, RenderableRawTypes.ONE_OF) and renderableType.className is not None:
134
129
  return self.object_type(isNullable=isNullable, className=renderableType.className, apiVersion=apiVersion, isLocalReference=isLocalReference)
135
130
  raise KibaException(f'Failed to render: {type(renderableType)} {renderableType}')
136
131
 
@@ -171,6 +166,23 @@ class BaseLanguageDefinition:
171
166
  def exception_class_name(self, exception: RenderableException, apiVersion: Version) -> str:
172
167
  return f'{self.pascal_name(name=exception.name)}V{apiVersion.major}'
173
168
 
169
+ def enum_class_name(self, enum: RenderableEnum, apiVersion: Version) -> str:
170
+ return f'{self.pascal_name(name=enum.name)}V{apiVersion.major}'
171
+
172
+ def one_of_class_name(self, oneOf: RenderableOneOf, apiVersion: Version) -> str:
173
+ return f'{self.pascal_name(name=oneOf.name)}V{apiVersion.major}'
174
+
175
+ def one_of_member_class_names(self, oneOf: RenderableOneOf, apiVersion: Version) -> list[str]:
176
+ return [f'{self.pascal_name(name=member.className)}V{apiVersion.major}' for member in oneOf.members]
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
 
@@ -327,16 +339,22 @@ class PythonLanguageDefinition(BaseLanguageDefinition):
327
339
  typeName = f'{className.pascalCaseString}V{apiVersion.major}' if isLocalReference else f'resources.{className.pascalCaseString}V{apiVersion.major}'
328
340
  return f'{typeName} | None' if isNullable else typeName
329
341
 
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
342
  def string_type(self, isNullable: bool) -> str:
335
343
  return 'str | None' if isNullable else 'str'
336
344
 
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'
345
+ def enum_type(self, isNullable: bool, renderableType: RenderableType, apiVersion: Version, isLocalReference: bool) -> str:
346
+ if renderableType.className is None:
347
+ typeName = self.enum_literal(values=renderableType.enumValues)
348
+ else:
349
+ typeName = f'{renderableType.className.pascalCaseString}V{apiVersion.major}' if isLocalReference else f'resources.{renderableType.className.pascalCaseString}V{apiVersion.major}'
350
+ return f'{typeName} | None' if isNullable else typeName
351
+
352
+ @staticmethod
353
+ def enum_literal(values: list[str]) -> str:
354
+ return f'typing.Literal[{", ".join(repr(value) for value in values)}]'
355
+
356
+ def file_type(self, isNullable: bool) -> str:
357
+ return 'UploadedFile | None' if isNullable else 'UploadedFile'
340
358
 
341
359
  def package_name(self, api: RenderableApi) -> str:
342
360
  return f'{self.snake_name(name=api.serviceName)}_api'
@@ -393,6 +411,19 @@ class PythonLanguageDefinition(BaseLanguageDefinition):
393
411
  def resources_require_json(self, api: RenderableApi) -> bool:
394
412
  return any(self._contains_json(renderableType=attribute.fieldType) for resource in api.resources for attribute in resource.attributes)
395
413
 
414
+ def resources_require_typing(self, api: RenderableApi) -> bool:
415
+ return len(api.enums) > 0 or len(api.oneOfs) > 0 or any(self.is_inline_enum(renderableType=attribute.fieldType) for resource in api.resources for attribute in resource.attributes)
416
+
417
+ def one_of_alias(self, oneOf: RenderableOneOf, apiVersion: Version) -> str:
418
+ return f'typing.Annotated[{" | ".join(self.one_of_member_class_names(oneOf=oneOf, apiVersion=apiVersion))}, pydantic.Field(discriminator={self.identifier(name=oneOf.discriminator)!r})]'
419
+
420
+ def operations_require_typing(self, api: RenderableApi) -> bool:
421
+ 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 []]
422
+ return any(self.is_inline_enum(renderableType=fieldType) for fieldType in fieldTypes)
423
+
424
+ def requires_uploaded_file(self, api: RenderableApi) -> bool:
425
+ return any(self.is_file_operation(operation=operation) for operation in api.operations)
426
+
396
427
  def operations_require_json(self, api: RenderableApi) -> bool:
397
428
  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
429
  return any(self._contains_json(renderableType=fieldType) for fieldType in fieldTypes)
@@ -516,6 +547,19 @@ class TypeScriptLanguageDefinition(BaseLanguageDefinition):
516
547
  def string_type(self, isNullable: bool) -> str:
517
548
  return self._nullable(typeName='string', isNullable=isNullable)
518
549
 
550
+ def enum_type(self, isNullable: bool, renderableType: RenderableType, apiVersion: Version, isLocalReference: bool) -> str:
551
+ if renderableType.className is None:
552
+ return self._nullable(typeName=f'({self.enum_union(values=renderableType.enumValues)})', isNullable=isNullable)
553
+ typeName = f'{renderableType.className.pascalCaseString}V{apiVersion.major}'
554
+ return self._nullable(typeName=typeName if isLocalReference else f'Resources.{typeName}', isNullable=isNullable)
555
+
556
+ @staticmethod
557
+ def enum_union(values: list[str]) -> str:
558
+ return ' | '.join(f"'{value}'" for value in values)
559
+
560
+ def file_type(self, isNullable: bool) -> str:
561
+ return self._nullable(typeName='File', isNullable=isNullable)
562
+
519
563
  def package_name(self, api: RenderableApi) -> str:
520
564
  return f'{self.kebab_name(name=api.serviceName)}-api'
521
565
 
@@ -566,7 +610,14 @@ class TypeScriptLanguageDefinition(BaseLanguageDefinition):
566
610
  return f'on{self.pascal_name(name=responseField.name)}'
567
611
 
568
612
  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))
613
+ valueTypes = [self._value_type(renderableType=renderableType) for renderableType in renderableTypes]
614
+ return any(valueType.rawType in ('class', PrimitiveTypes.JSON, RenderableRawTypes.ONE_OF) or (valueType.rawType == RenderableRawTypes.ENUM and valueType.className is not None) for valueType in valueTypes)
615
+
616
+ def one_of_parser_name(self, name: RenderableString, apiVersion: Version) -> str:
617
+ return f'{self._normalized_name(name=name).camelCaseString}V{apiVersion.major}FromObject'
618
+
619
+ def _value_type(self, renderableType: RenderableType) -> RenderableType:
620
+ return self._value_type(renderableType=renderableType.valueType) if isinstance(renderableType, MappingRenderableType) else renderableType
570
621
 
571
622
  def requires_date_parsing(self, renderableTypes: list[RenderableType]) -> bool:
572
623
  return any(self._contains_raw_type(renderableType=renderableType, rawType=rawType) for renderableType in renderableTypes for rawType in (PrimitiveTypes.DATE, PrimitiveTypes.DATETIME))
@@ -580,6 +631,18 @@ class TypeScriptLanguageDefinition(BaseLanguageDefinition):
580
631
  def body_parameter_types(self, api: RenderableApi) -> list[RenderableType]:
581
632
  return [parameter.fieldType for operation in self.client_operations(api=api) for parameter in self.body_parameters(operation=operation)]
582
633
 
634
+ def requires_request_data(self, api: RenderableApi) -> bool:
635
+ return any(self.body_parameters(operation=operation) and not self.is_file_operation(operation=operation) for operation in self.client_operations(api=api))
636
+
637
+ def form_value(self, renderableType: RenderableType, value: str) -> str:
638
+ if renderableType.rawType == RenderableRawTypes.FILE:
639
+ return value
640
+ if renderableType.rawType == PrimitiveTypes.DATETIME:
641
+ return f'dateToString({value})'
642
+ if renderableType.rawType == PrimitiveTypes.DATE:
643
+ return f"dateToString({value}, 'yyyy-MM-dd')"
644
+ return f'String({value})'
645
+
583
646
  def _contains_raw_type(self, renderableType: RenderableType, rawType: str) -> bool:
584
647
  if isinstance(renderableType, MappingRenderableType):
585
648
  return self._contains_raw_type(renderableType=renderableType.valueType, rawType=rawType)
@@ -597,6 +660,9 @@ class TypeScriptLanguageDefinition(BaseLanguageDefinition):
597
660
  valueTypeName = self.type(renderableType=renderableType.valueType, apiVersion=apiVersion, isLocalReference=isLocalReference)
598
661
  convertedEntry = self.from_object(renderableType=renderableType.valueType, value='entry', apiVersion=apiVersion, isLocalReference=isLocalReference)
599
662
  return f'Object.fromEntries(Object.entries({value} as RawObject).map(([key, entry]: [string, unknown]): [string, {valueTypeName}] => [key, {convertedEntry}]))'
663
+ if renderableType.rawType == RenderableRawTypes.ONE_OF and renderableType.className is not None:
664
+ parserName = self.one_of_parser_name(name=renderableType.className, apiVersion=apiVersion)
665
+ return f'{parserName if isLocalReference else f"Resources.{parserName}"}({value} as RawObject)'
600
666
  if renderableType.rawType == 'class' and renderableType.className is not None:
601
667
  return f'{self.type(renderableType=renderableType, apiVersion=apiVersion, isLocalReference=isLocalReference)}.fromObject({value} as RawObject)'
602
668
  if renderableType.rawType == PrimitiveTypes.STRING:
@@ -609,12 +675,14 @@ class TypeScriptLanguageDefinition(BaseLanguageDefinition):
609
675
  return f'dateFromString(String({value}))'
610
676
  if renderableType.rawType == PrimitiveTypes.JSON:
611
677
  return f'{value} as {self.type(renderableType=renderableType, apiVersion=apiVersion, isLocalReference=isLocalReference)}'
678
+ if renderableType.rawType == RenderableRawTypes.ENUM:
679
+ return f'String({value}) as {self.type(renderableType=renderableType, apiVersion=apiVersion, isLocalReference=isLocalReference)}'
612
680
  raise KibaException(f'Failed to render conversion from object for: {renderableType}')
613
681
 
614
682
  def _is_serialized_as_is(self, renderableType: RenderableType) -> bool:
615
683
  if isinstance(renderableType, MappingRenderableType):
616
684
  return self._is_serialized_as_is(renderableType=renderableType.valueType)
617
- return renderableType.rawType not in ('class', PrimitiveTypes.DATE, PrimitiveTypes.DATETIME)
685
+ return renderableType.rawType not in ('class', RenderableRawTypes.ONE_OF, PrimitiveTypes.DATE, PrimitiveTypes.DATETIME)
618
686
 
619
687
  def to_object(self, renderableType: RenderableType, value: str, apiVersion: Version, isOptional: bool = False, isLocalReference: bool = False) -> str:
620
688
  if self._is_serialized_as_is(renderableType=renderableType):
@@ -630,7 +698,7 @@ class TypeScriptLanguageDefinition(BaseLanguageDefinition):
630
698
  valueTypeName = self.type(renderableType=renderableType.valueType, apiVersion=apiVersion, isLocalReference=isLocalReference)
631
699
  convertedEntry = self.to_object(renderableType=renderableType.valueType, value='entry', apiVersion=apiVersion, isLocalReference=isLocalReference)
632
700
  return f'Object.fromEntries(Object.entries({value}).map(([key, entry]: [string, {valueTypeName}]): [string, unknown] => [key, {convertedEntry}]))'
633
- if renderableType.rawType == 'class':
701
+ if renderableType.rawType in ('class', RenderableRawTypes.ONE_OF):
634
702
  return f'{value}.toObject()'
635
703
  if renderableType.rawType == PrimitiveTypes.DATETIME:
636
704
  return f'dateToString({value})'
@@ -87,11 +87,35 @@ 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
+ ONE_OF = 'oneOf'
95
+
96
+
90
97
  class RenderableType(BaseModel):
91
98
  rawType: str
92
99
  isNullable: bool
93
100
  isList: bool = False
94
101
  className: RenderableString | None = None
102
+ enumValues: list[str] = []
103
+
104
+
105
+ class RenderableEnum(BaseModel):
106
+ name: RenderableString
107
+ values: list[str]
108
+
109
+
110
+ class RenderableOneOfMember(BaseModel):
111
+ className: RenderableString
112
+ value: str
113
+
114
+
115
+ class RenderableOneOf(BaseModel):
116
+ name: RenderableString
117
+ discriminator: RenderableString
118
+ members: list[RenderableOneOfMember]
95
119
 
96
120
 
97
121
  class MappingRenderableType(RenderableType):
@@ -225,6 +249,8 @@ class RenderableApi(BaseModel):
225
249
  resources: list[RenderableResource]
226
250
  operations: list[RenderableOperation]
227
251
  exceptions: list[RenderableException]
252
+ enums: list[RenderableEnum] = []
253
+ oneOfs: list[RenderableOneOf] = []
228
254
 
229
255
 
230
256
  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
- {% if target.resources_require_json(api=api) %}
10
+ {% if target.resources_require_json(api=api) or api.oneOfs %}
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):
@@ -20,3 +29,9 @@ class {{ target.resource_class_name(resource=resource, apiVersion=api.version) }
20
29
  {% endfor %}
21
30
 
22
31
  {% endfor %}
32
+ {% for oneOf in api.oneOfs %}
33
+ {% if loop.first %}
34
+
35
+ {% endif %}
36
+ {{ target.one_of_class_name(oneOf=oneOf, apiVersion=api.version) }} = {{ target.one_of_alias(oneOf=oneOf, apiVersion=api.version) }}
37
+ {% endfor %}
@@ -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.is_streaming(operation=operation) %}
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 bodyTypes %}{% set _ = coreImports.append('RequestData') %}{% endif %}
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
 
@@ -43,3 +47,20 @@ export class {{ className }} {
43
47
  {% endif %}
44
48
  }
45
49
  {% endfor %}
50
+ {% for oneOf in api.oneOfs %}
51
+ {% set typeName = target.one_of_class_name(oneOf=oneOf, apiVersion=api.version) %}
52
+ {% set discriminator = target.identifier(name=oneOf.discriminator) %}
53
+
54
+ export type {{ typeName }} = {{ target.one_of_member_class_names(oneOf=oneOf, apiVersion=api.version) | join(' | ') }};
55
+
56
+ export const {{ target.one_of_parser_name(name=oneOf.name, apiVersion=api.version) }} = (obj: RawObject): {{ typeName }} => {
57
+ switch (obj.{{ discriminator }}) {
58
+ {% for member in oneOf.members %}
59
+ case '{{ member.value }}':
60
+ return {{ target.one_of_member_class_names(oneOf=oneOf, apiVersion=api.version)[loop.index0] }}.fromObject(obj);
61
+ {% endfor %}
62
+ default:
63
+ throw new Error(`Unknown {{ typeName }} {{ discriminator }}: ${String(obj.{{ discriminator }})}`);
64
+ }
65
+ };
66
+ {% endfor %}
@@ -35,12 +35,31 @@ 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
+
50
+ class ApiOneOf(BaseModel):
51
+ name: str
52
+ discriminator: str
53
+ types: list[str]
54
+
55
+
38
56
  class Attribute(BaseModel):
39
57
  name: str
40
58
  fieldType: str = pydantic.Field(alias='type')
41
59
  listValueTypes: list[str] = []
42
60
  mappingKeyType: str | None = None
43
61
  mappingValueTypes: list[str] = []
62
+ enumValues: list[str] = []
44
63
  isPrimary: bool = False
45
64
  isUnique: bool = False
46
65
  shouldDisableGetEndpoint: bool = False
@@ -59,6 +78,7 @@ class ResponseField(BaseModel):
59
78
  listValueTypes: list[str] = []
60
79
  mappingKeyType: str | None = None
61
80
  mappingValueTypes: list[str] = []
81
+ enumValues: list[str] = []
62
82
  isNullable: bool = False
63
83
  isList: bool = False
64
84
 
@@ -69,6 +89,7 @@ class RequestField(BaseModel):
69
89
  listValueTypes: list[str] = []
70
90
  mappingKeyType: str | None = None
71
91
  mappingValueTypes: list[str] = []
92
+ enumValues: list[str] = []
72
93
  isRequired: bool = False
73
94
  isDeprecated: bool = False
74
95
  defaultValue: str | int | float | bool | None = None
@@ -139,6 +160,8 @@ class KibaApi(BaseModel):
139
160
  resources: list[Resource] = []
140
161
  transitions: list[RootTransition] = []
141
162
  exceptions: list[ApiException] = []
163
+ enums: list[ApiEnum] = []
164
+ oneOfs: list[ApiOneOf] = []
142
165
  defaultAuthentication: str | None = None
143
166
 
144
167
  @property