-
Notifications
You must be signed in to change notification settings - Fork 68
Description
Today I learned that enum values in Rust don't have to be strings; they can include tuple and object structs too. In #88, the ApiInstanceState enum was modified to include structs:
pub enum ApiInstanceState {
Creating,
Starting,
Running,
- Stopping
- Stopped
+ Stopping { rebooting: bool },
+ Stopped { rebooting: bool },
Repairing,
Failed,
Destroyed,
}Here I would like to:
- ask whether the resulting JSON schema is what we expect based on the Rust code (I think it is)
- ask whether the resulting JSON schema is something we want clients to have to handle (not sure)
- note that regardless of whether we like the schema, the TypeScript client generator is handling it very badly
Generated JSON schema
The generated schema is an anyOf containing:
- an enum of the string values
- an object that looks like
{ stopping: { rebooting: true/false } } - an object that looks like
{ stoppped: { rebooting: true/false } }
I'm no OpenAPI expert, but this seems right to me. I don't know how else you would do it. OpenAPI enums can only have string values.
Expand for JSON
"ApiInstanceState": {
"description": "blah blah blah",
"anyOf": [
{
"type": "string",
"enum": [
"creating",
"starting",
"running",
"repairing",
"failed",
"destroyed"
]
},
{
"type": "object",
"properties": {
"stopping": {
"type": "object",
"properties": {
"rebooting": {
"type": "boolean"
}
},
"required": ["rebooting"]
}
},
"required": ["stopping"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"stopped": {
"type": "object",
"properties": {
"rebooting": {
"type": "boolean"
}
},
"required": ["rebooting"]
}
},
"required": ["stopped"],
"additionalProperties": false
}
]
},
Generated TypeScript client
The client generator, on the other hand, gets it completely wrong. I get something like the following (abbreviated for convenience) with no connection between ApiInstanceStateAnyOf and ApiInstanceState. It's totally broken, but as far as I can tell this is not Dropshot or Nexus's fault at all.
enum ApiInstanceStateAnyOf {
Creating = 'creating',
Starting = 'starting',
Running = 'running',
Repairing = 'repairing',
Failed = 'failed',
Destroyed = 'destroyed',
}
interface ApiInstanceStateAnyOf1Stopping {
rebooting: boolean
}
interface ApiInstanceState {
stopping: ApiInstanceStateAnyOf1Stopping
stopped: ApiInstanceStateAnyOf1Stopping
}Instead I would expect something like this:
type ApiInstanceState =
| 'creating'
| 'starting'
| 'running'
| 'repairing'
| 'failed'
| 'destroyed'
| { stopped: { rebooting: boolean } }
| { stopping: { rebooting: boolean } }
// both valid
const x: ApiInstanceState = 'creating'
const y: ApiInstanceState = { stopped: { rebooting: true } }Would it be good even if the generated client was correct
Mixing strings and objects in a single union is a little odd. It's not that bad and the typechecker will help you out a lot, but someone working with a client in a dynamic language would have a hard time getting these values right.
Mixing strings and objects also makes display logic more complicated: if it's a string you can display it directly, otherwise you have to look inside the object and pull out stopped/stopping and rebooting.
Options
- Avoid using structs in enums that are going straight into the API schema
- The above would have to be modeled as something like
stopped,stopped_rebooting,stopping,stopping_rebooting, which is a bit ugly but conceptually pretty much the same
- The above would have to be modeled as something like
- Fix TS client generator (quite painful)
- Manually create the correct type for the client (slippery slope)
- Stitch together the correct type from the generated types (maybe slightly less slippery of a slope because it at least has to cohere with the generated code)