geniespec 0.1.1.dev7__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.dev7 → geniespec-0.1.1.dev8}/PKG-INFO +20 -4
  2. {geniespec-0.1.1.dev7 → geniespec-0.1.1.dev8}/README.md +19 -3
  3. {geniespec-0.1.1.dev7 → geniespec-0.1.1.dev8}/pyproject.toml +1 -1
  4. {geniespec-0.1.1.dev7 → geniespec-0.1.1.dev8}/pyproject.toml.orig +1 -1
  5. {geniespec-0.1.1.dev7 → geniespec-0.1.1.dev8}/src/geniespec/api_schema.yaml +25 -0
  6. {geniespec-0.1.1.dev7 → geniespec-0.1.1.dev8}/src/geniespec/genie.py +38 -2
  7. {geniespec-0.1.1.dev7 → geniespec-0.1.1.dev8}/src/geniespec/languages.py +21 -16
  8. {geniespec-0.1.1.dev7 → geniespec-0.1.1.dev8}/src/geniespec/renderable_types.py +13 -0
  9. {geniespec-0.1.1.dev7 → geniespec-0.1.1.dev8}/src/geniespec/templates/python-server/{service}/v{version}/resources.py.j2 +7 -1
  10. {geniespec-0.1.1.dev7 → geniespec-0.1.1.dev8}/src/geniespec/templates/typescript-client/{service}/v{version}/resources.ts.j2 +17 -0
  11. {geniespec-0.1.1.dev7 → geniespec-0.1.1.dev8}/src/geniespec/types.py +7 -0
  12. {geniespec-0.1.1.dev7 → geniespec-0.1.1.dev8}/src/geniespec/__init__.py +0 -0
  13. {geniespec-0.1.1.dev7 → geniespec-0.1.1.dev8}/src/geniespec/cli.py +0 -0
  14. {geniespec-0.1.1.dev7 → geniespec-0.1.1.dev8}/src/geniespec/templates/python-server/{service}/__init__.py.j2 +0 -0
  15. {geniespec-0.1.1.dev7 → geniespec-0.1.1.dev8}/src/geniespec/templates/python-server/{service}/v{version}/__init__.py.j2 +0 -0
  16. {geniespec-0.1.1.dev7 → geniespec-0.1.1.dev8}/src/geniespec/templates/python-server/{service}/v{version}/api.py.j2 +0 -0
  17. {geniespec-0.1.1.dev7 → geniespec-0.1.1.dev8}/src/geniespec/templates/python-server/{service}/v{version}/endpoints.py.j2 +0 -0
  18. {geniespec-0.1.1.dev7 → geniespec-0.1.1.dev8}/src/geniespec/templates/python-server/{service}/v{version}/exceptions.py.j2 +0 -0
  19. {geniespec-0.1.1.dev7 → geniespec-0.1.1.dev8}/src/geniespec/templates/python-server/{service}/v{version}/internal.py.j2 +0 -0
  20. {geniespec-0.1.1.dev7 → geniespec-0.1.1.dev8}/src/geniespec/templates/typescript-client/{service}/v{version}/client.ts.j2 +0 -0
  21. {geniespec-0.1.1.dev7 → geniespec-0.1.1.dev8}/src/geniespec/templates/typescript-client/{service}/v{version}/endpoints.ts.j2 +0 -0
  22. {geniespec-0.1.1.dev7 → 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.dev7
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
@@ -27,7 +27,7 @@ geniespec watch --spec api.yaml --target <python-server | typescript-client> [--
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
30
+ ## Enums, oneOfs and files
31
31
 
32
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
33
 
@@ -50,6 +50,24 @@ resources:
50
50
 
51
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
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
+
53
71
  # Development
54
72
 
55
73
  ```
@@ -70,8 +88,6 @@ Merges to `main` publish a dev version to PyPI. To release, bump `version` in `p
70
88
 
71
89
  ## Typing of params and fields in general
72
90
 
73
- OneOf not supported yet
74
-
75
91
  I think we need to create an object for Type. what would it look like?
76
92
 
77
93
 
@@ -13,7 +13,7 @@ geniespec watch --spec api.yaml --target <python-server | typescript-client> [--
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
16
+ ## Enums, oneOfs and files
17
17
 
18
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
19
 
@@ -36,6 +36,24 @@ resources:
36
36
 
37
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
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
+
39
57
  # Development
40
58
 
41
59
  ```
@@ -56,8 +74,6 @@ Merges to `main` publish a dev version to PyPI. To release, bump `version` in `p
56
74
 
57
75
  ## Typing of params and fields in general
58
76
 
59
- OneOf not supported yet
60
-
61
77
  I think we need to create an object for Type. what would it look like?
62
78
 
63
79
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "geniespec"
3
- version = "0.1.1.dev7"
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"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "geniespec"
3
- version = "0.1.1.dev7"
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"
@@ -36,6 +36,10 @@ properties:
36
36
  type: array
37
37
  items:
38
38
  $ref: '#/definitions/Enum'
39
+ oneOfs:
40
+ type: array
41
+ items:
42
+ $ref: '#/definitions/OneOf'
39
43
  imports:
40
44
  type: array
41
45
  items:
@@ -65,6 +69,27 @@ definitions:
65
69
  pattern: '^[A-Z][A-Za-z]*$'
66
70
  values:
67
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-]*$'
68
93
  Attribute:
69
94
  type: object
70
95
  additionalProperties: false
@@ -28,6 +28,8 @@ from geniespec.renderable_types import RenderableEnum
28
28
  from geniespec.renderable_types import RenderableException
29
29
  from geniespec.renderable_types import RenderableField
30
30
  from geniespec.renderable_types import RenderableMethod
31
+ from geniespec.renderable_types import RenderableOneOf
32
+ from geniespec.renderable_types import RenderableOneOfMember
31
33
  from geniespec.renderable_types import RenderableOperation
32
34
  from geniespec.renderable_types import RenderableParameter
33
35
  from geniespec.renderable_types import RenderableRawTypes
@@ -36,6 +38,8 @@ from geniespec.renderable_types import RenderableString
36
38
  from geniespec.renderable_types import RenderableType
37
39
  from geniespec.renderable_types import ResourceEndpointComponent
38
40
  from geniespec.renderable_types import SingleResourceEndpointComponent
41
+ from geniespec.types import ApiEnum
42
+ from geniespec.types import ApiOneOf
39
43
  from geniespec.types import Attribute
40
44
  from geniespec.types import BaseTransition
41
45
  from geniespec.types import ComplexTypes
@@ -378,13 +382,44 @@ class Renderer:
378
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]
379
383
  return errors
380
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
+
381
406
  def build_renderable_api(self, api: KibaApi) -> RenderableApi:
382
407
  fullResourceNames = [self._resource_full_name(resource=resource, name=resource.singleName) for resource in api.resources]
383
408
  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]
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]
386
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]
387
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))
388
423
  for resource, fullName in zip(api.resources, fullResourceNames, strict=True):
389
424
  ancestorNames: list[str] = []
390
425
  ancestor = resourcesByName.get(resource.parentResource) if resource.parentResource is not None else None
@@ -439,6 +474,7 @@ class Renderer:
439
474
  operations=renderableOperations,
440
475
  exceptions=renderableExceptions,
441
476
  enums=[RenderableEnum(name=RenderableString.from_pascal_case_string(string=enum.name), values=enum.values) for enum in api.enums],
477
+ oneOfs=renderableOneOfs,
442
478
  )
443
479
  renderableResources: list[RenderableResource] = [self._resource_to_renderable_resource(resource=resource, knownTypeNames=knownTypeNames) for resource in api.resources]
444
480
  uncoveredTransitions: list[str] = []
@@ -12,6 +12,7 @@ from geniespec.renderable_types import RenderableEndpointComponent
12
12
  from geniespec.renderable_types import RenderableEnum
13
13
  from geniespec.renderable_types import RenderableException
14
14
  from geniespec.renderable_types import RenderableField
15
+ from geniespec.renderable_types import RenderableOneOf
15
16
  from geniespec.renderable_types import RenderableOperation
16
17
  from geniespec.renderable_types import RenderableParameter
17
18
  from geniespec.renderable_types import RenderableRawTypes
@@ -68,9 +69,6 @@ class BaseLanguageDefinition:
68
69
  def object_type(self, isNullable: bool, className: RenderableString, apiVersion: Version, isLocalReference: bool) -> str:
69
70
  raise NotImplementedError
70
71
 
71
- def one_of_type(self, isNullable: bool, subTypes: list[str]) -> str:
72
- raise NotImplementedError
73
-
74
72
  def string_type(self, isNullable: bool) -> str:
75
73
  raise NotImplementedError
76
74
 
@@ -92,10 +90,6 @@ class BaseLanguageDefinition:
92
90
  def is_valid_name(self, name: str) -> bool:
93
91
  raise NotImplementedError
94
92
 
95
- def _render_one_of_type_string(self, oneOfTypeString: str, isNullable: bool, apiVersion: Version) -> list[str]:
96
- oneOfTypes = ''.join(oneOfTypeString.replace('OneOf[', '', 1).rsplit(']', 1)).split(', ')
97
- return [self.render_type(renderableType=RenderableType(rawType=oneOfType, isNullable=isNullable), apiVersion=apiVersion) for oneOfType in oneOfTypes]
98
-
99
93
  def _render_list_type_string(self, listTypeString: str, apiVersion: Version) -> str:
100
94
  listItemType = ''.join(listTypeString.replace('List[', '', 1).rsplit(']', 1))
101
95
  return self.render_type(renderableType=RenderableType(rawType=listItemType, isNullable=False), apiVersion=apiVersion)
@@ -131,7 +125,7 @@ class BaseLanguageDefinition:
131
125
  keyType = self.render_type(renderableType=renderableType.keyType, apiVersion=apiVersion, isLocalReference=isLocalReference)
132
126
  valueType = self.render_type(renderableType=renderableType.valueType, apiVersion=apiVersion, isLocalReference=isLocalReference)
133
127
  return self.mapping_type(isNullable=isNullable, keyType=keyType, valueType=valueType)
134
- 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:
135
129
  return self.object_type(isNullable=isNullable, className=renderableType.className, apiVersion=apiVersion, isLocalReference=isLocalReference)
136
130
  raise KibaException(f'Failed to render: {type(renderableType)} {renderableType}')
137
131
 
@@ -175,6 +169,12 @@ class BaseLanguageDefinition:
175
169
  def enum_class_name(self, enum: RenderableEnum, apiVersion: Version) -> str:
176
170
  return f'{self.pascal_name(name=enum.name)}V{apiVersion.major}'
177
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
178
  @staticmethod
179
179
  def is_inline_enum(renderableType: RenderableType) -> bool:
180
180
  return renderableType.rawType == RenderableRawTypes.ENUM and renderableType.className is None
@@ -339,10 +339,6 @@ class PythonLanguageDefinition(BaseLanguageDefinition):
339
339
  typeName = f'{className.pascalCaseString}V{apiVersion.major}' if isLocalReference else f'resources.{className.pascalCaseString}V{apiVersion.major}'
340
340
  return f'{typeName} | None' if isNullable else typeName
341
341
 
342
- def one_of_type(self, isNullable: bool, subTypes: list[str]) -> str:
343
- fieldTypes = ' | '.join(subTypes)
344
- return f'{fieldTypes} | None' if isNullable else f'Union[{fieldTypes} | None'
345
-
346
342
  def string_type(self, isNullable: bool) -> str:
347
343
  return 'str | None' if isNullable else 'str'
348
344
 
@@ -416,7 +412,10 @@ class PythonLanguageDefinition(BaseLanguageDefinition):
416
412
  return any(self._contains_json(renderableType=attribute.fieldType) for resource in api.resources for attribute in resource.attributes)
417
413
 
418
414
  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)
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})]'
420
419
 
421
420
  def operations_require_typing(self, api: RenderableApi) -> bool:
422
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 []]
@@ -612,7 +611,10 @@ class TypeScriptLanguageDefinition(BaseLanguageDefinition):
612
611
 
613
612
  def references_resources(self, renderableTypes: list[RenderableType]) -> bool:
614
613
  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)
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'
616
618
 
617
619
  def _value_type(self, renderableType: RenderableType) -> RenderableType:
618
620
  return self._value_type(renderableType=renderableType.valueType) if isinstance(renderableType, MappingRenderableType) else renderableType
@@ -658,6 +660,9 @@ class TypeScriptLanguageDefinition(BaseLanguageDefinition):
658
660
  valueTypeName = self.type(renderableType=renderableType.valueType, apiVersion=apiVersion, isLocalReference=isLocalReference)
659
661
  convertedEntry = self.from_object(renderableType=renderableType.valueType, value='entry', apiVersion=apiVersion, isLocalReference=isLocalReference)
660
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)'
661
666
  if renderableType.rawType == 'class' and renderableType.className is not None:
662
667
  return f'{self.type(renderableType=renderableType, apiVersion=apiVersion, isLocalReference=isLocalReference)}.fromObject({value} as RawObject)'
663
668
  if renderableType.rawType == PrimitiveTypes.STRING:
@@ -677,7 +682,7 @@ class TypeScriptLanguageDefinition(BaseLanguageDefinition):
677
682
  def _is_serialized_as_is(self, renderableType: RenderableType) -> bool:
678
683
  if isinstance(renderableType, MappingRenderableType):
679
684
  return self._is_serialized_as_is(renderableType=renderableType.valueType)
680
- return renderableType.rawType not in ('class', PrimitiveTypes.DATE, PrimitiveTypes.DATETIME)
685
+ return renderableType.rawType not in ('class', RenderableRawTypes.ONE_OF, PrimitiveTypes.DATE, PrimitiveTypes.DATETIME)
681
686
 
682
687
  def to_object(self, renderableType: RenderableType, value: str, apiVersion: Version, isOptional: bool = False, isLocalReference: bool = False) -> str:
683
688
  if self._is_serialized_as_is(renderableType=renderableType):
@@ -693,7 +698,7 @@ class TypeScriptLanguageDefinition(BaseLanguageDefinition):
693
698
  valueTypeName = self.type(renderableType=renderableType.valueType, apiVersion=apiVersion, isLocalReference=isLocalReference)
694
699
  convertedEntry = self.to_object(renderableType=renderableType.valueType, value='entry', apiVersion=apiVersion, isLocalReference=isLocalReference)
695
700
  return f'Object.fromEntries(Object.entries({value}).map(([key, entry]: [string, {valueTypeName}]): [string, unknown] => [key, {convertedEntry}]))'
696
- if renderableType.rawType == 'class':
701
+ if renderableType.rawType in ('class', RenderableRawTypes.ONE_OF):
697
702
  return f'{value}.toObject()'
698
703
  if renderableType.rawType == PrimitiveTypes.DATETIME:
699
704
  return f'dateToString({value})'
@@ -91,6 +91,7 @@ class RenderableRawTypes:
91
91
  CLASS = 'class'
92
92
  ENUM = 'enum'
93
93
  FILE = 'file'
94
+ ONE_OF = 'oneOf'
94
95
 
95
96
 
96
97
  class RenderableType(BaseModel):
@@ -106,6 +107,17 @@ class RenderableEnum(BaseModel):
106
107
  values: list[str]
107
108
 
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]
119
+
120
+
109
121
  class MappingRenderableType(RenderableType):
110
122
  keyType: RenderableType
111
123
  valueType: RenderableType
@@ -238,6 +250,7 @@ class RenderableApi(BaseModel):
238
250
  operations: list[RenderableOperation]
239
251
  exceptions: list[RenderableException]
240
252
  enums: list[RenderableEnum] = []
253
+ oneOfs: list[RenderableOneOf] = []
241
254
 
242
255
 
243
256
  class ProcessableResource(BaseModel):
@@ -7,7 +7,7 @@ import datetime
7
7
  import typing
8
8
  {% endif %}
9
9
 
10
- {% if target.resources_require_json(api=api) %}
10
+ {% if target.resources_require_json(api=api) or api.oneOfs %}
11
11
  import pydantic
12
12
  {% endif %}
13
13
  from pydantic import BaseModel
@@ -29,3 +29,9 @@ class {{ target.resource_class_name(resource=resource, apiVersion=api.version) }
29
29
  {% endfor %}
30
30
 
31
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 %}
@@ -47,3 +47,20 @@ export class {{ className }} {
47
47
  {% endif %}
48
48
  }
49
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 %}
@@ -47,6 +47,12 @@ class ApiEnum(BaseModel):
47
47
  values: list[str]
48
48
 
49
49
 
50
+ class ApiOneOf(BaseModel):
51
+ name: str
52
+ discriminator: str
53
+ types: list[str]
54
+
55
+
50
56
  class Attribute(BaseModel):
51
57
  name: str
52
58
  fieldType: str = pydantic.Field(alias='type')
@@ -155,6 +161,7 @@ class KibaApi(BaseModel):
155
161
  transitions: list[RootTransition] = []
156
162
  exceptions: list[ApiException] = []
157
163
  enums: list[ApiEnum] = []
164
+ oneOfs: list[ApiOneOf] = []
158
165
  defaultAuthentication: str | None = None
159
166
 
160
167
  @property