Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update pinned omicron version #474

Merged
merged 10 commits into from
Oct 1, 2021
Merged

Update pinned omicron version #474

merged 10 commits into from
Oct 1, 2021

Conversation

zephraph
Copy link
Contributor

@zephraph zephraph commented Sep 27, 2021

This PR updates to the latest omicron version. There's a few caveats.

Once GREsau/schemars#108 and oxidecomputer/omicron#273 were merged our schema generation updated to use oneOf instead of anyOf in cases where we want to represent a discriminated union. Unfortunately the open-api generator we're using didn't interpret the spec change in the way we'd hoped. You can see some of the context on that below.

To combat the undesirable generation I've added a temporary measure of running codemods as a post generation step to result in code closer to what we want. It's still not ideal and we may end up migrating over to swagger-typescript-api eventually, but these changes unblock us now in a way that will continue to work even if new disk states are added.


I'll note that as mentioned by @david-crespo below, the JSON marshaling doesn't exactly work in the best way. I'd considered making a more sophisticated codemod to handle that but decided that isn't the best use of my time right now.

@vercel
Copy link

vercel bot commented Sep 27, 2021

This pull request is being automatically deployed with Vercel (learn more).
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: https://vercel.com/oxidecomputer/console-ui-storybook/BVVavrWKbmC3JrTYZmetHZZRmcsv
✅ Preview: https://console-ui-storybook-git-update-api-client-oxidecomputer.vercel.app

@github-actions
Copy link
Contributor

Preview will be deployed at https://console-git-update-api-client.internal.oxide.computer

@zephraph zephraph temporarily deployed to Preview VM September 27, 2021 20:12 Inactive
| DiskStateOneOf3
| DiskStateOneOf4
| DiskStateOneOf5
| DiskStateOneOf6
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking gooder

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ish, heh. The nested enum isn't the best. It doesn't really resolve for type completion. It seems like this should be a singular enum instead of what it's producing (a union of single member enums).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I hadn't seen the nested enum, that's terrible. I don't mind using a discriminated union at top level, but I would want the arms to use strings instead of enums, like { state: 'creating' } | { state: 'detached' } | ...

Copy link
Contributor Author

@zephraph zephraph Oct 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, it's updated to do this now. I've added codemods to patch up (most) of the changes we wanted which includes ensuring there's actually a string discriminant instead of a single element enum. See this codemod. It updates it for all oneOf models including SagaErrorInfo and SagaState. Any new models that implement this same pattern will also be patched automatically.

...DiskStateOneOf3ToJSON(value),
...DiskStateOneOf4ToJSON(value),
...DiskStateOneOf5ToJSON(value),
...DiskStateOneOf6ToJSON(value),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is awful haha. pretty sure the logic is wrong in addition to typecheck failing. 3 returns

{
    instance: value.instance,
    state: value.state,
}

so even if you're on an arm that doesn't have an instance key, you're still going to get instance: undefined in the result.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

however, doing it right sounds kind of sophisticated because it has to know to use state at runtime as a discriminator to choose which serializer to use. urg

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

turns out I noticed the to/from JSON weirdness and forgot about it
#458 (comment)

Also notable: swagger-typescript-api produces a perfect model for DiskState BUT it provides no special to/from JSON. For parsing responses, it just sticks the response JSON in an object with the type, and doesn't, e.g., convert timestamps to Date. And for serializing to JSON it provides nothing, which is maybe fine since we're not actually using that anyway?
https://github.com/oxidecomputer/console/blob/sta/gen-client/Api.ts#L118-L128

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I did some deeper exploration with swagger-typescript-api which definitely does produce better type results. The overall API ergonomics itself become difficult though. It automatically namespaces methods based off of routes which is a little annoying:

hardware = {

I eventually figured out a way around that using their internal hooks.

Here's a build script if you want to toy with it: https://gist.github.com/zephraph/db8e8f73a4d9c887a08e838ed6057774

More to the point though, after looking a bit more at the openapi description the output that we're seeing isn't totally unreasonable.

{
  "DiskState": {
    "description": "State of a Disk (primarily: attached or not)",
    "oneOf": [
      {
        "description": "Disk is being initialized",
        "type": "object",
        "properties": {
          "state": {
            "type": "string",
            "enum": ["creating"]
          }
        },
        "required": ["state"]
      }
    ]
  }
}

It's taking the naive approach here and viewing every enum as a unique instance of an enum. Given what I'm seeing from the documentation this is likely the output is likely correct to spec even if it's not what we actually want.

oneOf validates the value against exactly one of the subschemas
-- source

This is perhaps something we'll need to do some custom generation for. I assume that means modifying a template to produce our desired outcome.

Copy link
Contributor Author

@zephraph zephraph Sep 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, negative on the template. That's more for formatting output than it is for making logical changes.

https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator/src/main/resources/typescript-fetch

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, so here's the thing... I'm not convinced openapi actually allows us to express what we're wanting to express here. I've been trying to generate the output in other languages just to see what would happen and I'm seeing similar results. If multiple language outputs for the official tool spits out bad results it points more towards our original openapi spec being bad.

Maybe a better exercise for us to go through is what does the openapi spec need to look like for us to get the generated output we want? We can work our way up from there... back to the generator where I feel is ultimately the place we need to do the change.

Ultimately, in my mind, we want the output of the official generator to be correct. If it's not then we'll be fighting the ecosystem and ultimately have a worse SDK experience for our end users.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, it seems like the spec is bad and we need to fix that. Turns out OpenAPI does support specifying a "discriminator" property, which is exactly what state is functioning as here.

Of course, we'll have to see if the TS generator knows what to do with that. Looks like it might. Note also that the config page mentions a setting for "legacy discriminator behavior".

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be more specific about how to improve the spec:

  1. It should specify that state is a discriminator

  2. This bit should maybe not be an enum (unless that's correct way to say it has one specific value)

    "state": {
      "type": "string",
      "enum": ["creating"]
    }
    

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will discuss tomorrow. Inline schemas don't work with discriminator, so each arm of the oneOf would have to be pulled out as its own schema instead of inlined. Also it seems from the docs that you can't just use state as the discriminator, the value of the discriminator has to be the name of the schema. So it would have to be something like DiskState_Creating, etc. Gnarly.

93254f0

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'd tried the discriminator field too last night but the result definitely isn't helpful.

@zephraph zephraph temporarily deployed to Preview VM October 1, 2021 00:07 Inactive
@zephraph zephraph temporarily deployed to Preview VM October 1, 2021 00:11 Inactive
@zephraph zephraph temporarily deployed to Preview VM October 1, 2021 01:04 Inactive
@zephraph zephraph temporarily deployed to Preview VM October 1, 2021 02:11 Inactive
@zephraph zephraph temporarily deployed to Preview VM October 1, 2021 02:19 Inactive
.replaceWith(j.literal(stateName))
.toSource()
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wild! Never written a codemod before. Very clean.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're incredibly useful tools. I've been meaning to write a blog post on it for a while, might do that this weekend.

...DiskStateOneOf3ToJSON(value as any),
...DiskStateOneOf4ToJSON(value as any),
...DiskStateOneOf5ToJSON(value as any),
...DiskStateOneOf6ToJSON(value as any),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it too fiddly to get the codemod to do the even more correct thing here? probably, right? like the codemod would basically have to encode the mapping from states to all the subtypes

switch (value.state) {
  case 'creating':
    return DiskStateOneOfToJSON(value)
  case 'detached':
    return DiskStateOneOf1ToJSON(value)
  // etc
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh haha I just saw your note at the end of the PR description

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can do it. It'd probably would only take an hour or two. We can follow up on it.

@zephraph zephraph temporarily deployed to Preview VM October 1, 2021 15:56 Inactive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants