Skip to content

Problem with structs in ApiInstanceState enum #117

@david-crespo

Description

@david-crespo

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:

  1. ask whether the resulting JSON schema is what we expect based on the Rust code (I think it is)
  2. ask whether the resulting JSON schema is something we want clients to have to handle (not sure)
  3. 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
  • 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)

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions