-
Notifications
You must be signed in to change notification settings - Fork 42
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
oneOf doesn't really make sense in other languages #573
Comments
Just to be verbose this is on more than just DiskState it is the following:
|
I'm trying to parse the result of oxidecomputer/dropshot#126 - I was under the impression that we concluded:
I'm kinda having a gut reaction of "if oneOf doesn't make sense in any languages (other than Rust), why is it a part of OpenAPI?!?", but digging around, it's unclear to me what portion of I'm not sure what the right next step is - it kinda seems to me like we're generating a "technically correct" spec, and it should be up to the language-specific generators to do the right thing.
We can always change the API exposed by Omicron, but I guess I'm just trying to get a solid confirmation of "what is okay" vs "what is not okay" to create in an OpenAPI spec, so we make this change as few times as possible. |
And, just to be explicit: I'm aware that at the end of the day, generated client bindings are our API, so they should be prioritized over the Rust-specific fit. That being said, these enums do have data that only makes sense with a single variant, so even from an OpenAPI point-of-view, |
its more like oneOf is a huge pain in the ass IMO for generated client and users of said clients, like why make our users suffer when we could just not do weird shit like a rust-ism like ENUM type |
its not like its impossible but its gross and we should aim for non-gross clients, clients that are idiomatic in their respective languages... |
I think the The TS generator handles these nicely because it has sum types. I see that Go doesn't have sum types, and there are various workarounds people use to simulate them. Is none of the workarounds good enough? export type DiskState =
| { state: 'creating' }
| { state: 'detached' }
| { state: 'attaching'; instance: string }
| { state: 'attached'; instance: string }
| { state: 'detaching'; instance: string }
| { state: 'destroyed' }
| { state: 'faulted' } |
The way we are writing DiskState is a rust-ism. literally that type of enum is a rust ism! The goal is for our customers to not suffer. I do not want to debate this dumb shit, we want idiomatic clients. period. I don't give a shit about rust-ism or not rust-ism but I want to be able to generate idiomatic clients, end of story. |
Is this not idiomatic OpenAPI independent of the Rust that generated it? What should it look like instead? Lines 3148 to 3272 in f3e7eb1
|
I'm saying instead of doing omicron/common/src/api/external/mod.rs Line 851 in 656d3ad
|
If we were to eliminate the
That makes it a much worse representation in the many languages that do have sum types because you don't know at the type level which states have instances and which don't. So it's not a simple question of what's better for clients: if we make Go much better we make Rust and TS much worse. |
Would it not be possible to represent the client in such a way that you could use the "type switch" construct? I tried to knock together an example of what I mean with package main
import "fmt"
/*
* RouteDestination
*/
type RouteDestination interface {
Type() string
}
/*
* RouteDestination variant "ip":
*/
type RouteDestinationIP struct {
Ip string
}
func (rds RouteDestinationIP) String() string {
return fmt.Sprintf("%s:%s", rds.Type(), rds.Ip)
}
func (rds RouteDestinationIP) Type() string {
return "ip"
}
/*
* RouteDestination variant "vpc":
*/
type RouteDestinationVPC struct {
Name string
}
func (rds RouteDestinationVPC) String() string {
return fmt.Sprintf("%s:%s", rds.Type(), rds.Name)
}
func (rds RouteDestinationVPC) Type() string {
return "vpc"
}
/*
* RouteDestination variant "subnet":
*/
type RouteDestinationSubnet struct {
Name string
}
func (rds RouteDestinationSubnet) String() string {
return fmt.Sprintf("%s:%s", rds.Type(), rds.Name)
}
func (rds RouteDestinationSubnet) Type() string {
return "subnet"
}
/*
* Our consumer:
*/
func main() {
destinations := []RouteDestination{
RouteDestinationVPC{Name: "blah"},
RouteDestinationIP{Ip: "127.0.0.1"},
RouteDestinationSubnet{Name: "farend"},
}
for i, rd := range destinations {
fmt.Println("[", i, "]", rd, "which is variant type", rd.Type())
switch rd := rd.(type) {
case RouteDestinationSubnet:
fmt.Printf("\ta subnet, \"%s\", my favourite!\n", rd.Name)
case RouteDestinationIP:
fmt.Printf("\ta direct IP address, %s!\n", rd.Ip)
case RouteDestinationVPC:
fmt.Printf("\ton second thought let's not go to %s, it is a silly place\n", rd.Name)
}
}
} When run, this outputs:
I put this up on the Go playground as well: https://go.dev/play/p/zKoNbVnoGcR This would be laborious to type out by hand, of course, but it seems like the code generator could make short work of it. Interfaces and the type switch pattern are mentioned in the Go tutorial, so I imagine they are at least somewhat acceptable? |
I mean its not that hard either make it an enum of all the same type (strings) or make it a struct where there's a nested enum and the attaching/detaching shit there. The way it is today just creates weird shit for all other languages |
@jclulow why do that when we could just have an object/struct instead |
honestly the type checking thing is fucking gross imo |
I'm genuinely curious - why? Even within just the context of Go, there seems to be a tradeoff between:
|
heres the deal, if one of you volunteers to do the go client (from what I've started https://github.com/oxidecomputer/oxide.go) and the k8s integration, and the hashicorp packer/terraform integration (what consumes the go client) so you can feel the shitty-ness you can have it your way, otherwise I will do those things but I require a nice client before doing it ;) |
If I'm understanding right, @jessfraz you're saying that the sum types that we're using in the API spec are Rust-isms that are awkward in all other languages and so we should avoid using them in the API? For what it's worth, sum types aren't Rust-specific -- they've been around since at least the 60s, there are established patterns for using them in languages like C, and they appear to have good support in TypeScript, Swift, Scala, and recent Python. That @jessfraz wrote:
In case it's not clear why folks prefer to use sum types in the API: the reason to use sum types is that they more precisely specify constraints on a value, which facilitates better type checking and so (we hope) a much better user experience. Without sum types, a client needs to do extra validation on top of what the API spec says. In many languages, it's easy to forget to do that and then introduce bugs (e.g., you forget to check if some null-able field is null and then the program crashes at runtime). The extra validation rules also aren't written down anywhere -- there's no way to really know what you need to check. By foregoing this more precise representation in the API spec, we'd be making it easier to introduce bugs not just in languages that don't support these types well, but also the languages that do, including two that we use heavily within the product (Rust and TypeScript). I like the way @david-crespo said it above:
and to @smklein's point above, there are good reasons even in Go that someone might prefer the stronger types. @jessfraz wrote:
Is it clear what's idiomatic in Go for this situation? From the links in this thread it seems like at least some folks in Go do use sum type patterns -- enough of them that it's even in the Tour of Go. |
Honestly it’s the difference between our customers having to type check in Go every time they call a method that returns one of these types versus customers having to across all languages get back an object for disk state with an enum of strings and an optional instance.
It’s like 2 lines of code versus 3*(n # of types) for go. And as an avid user of Go, and how a lot of cloud software and users write go, I would think we would do the former. The trade off will have the most value for our customers.
… On Jan 3, 2022, at 3:45 PM, David Pacheco ***@***.***> wrote:
If I'm understanding right, @jessfraz you're saying that the sum types that we're using in the API spec are Rust-isms that are awkward in all other languages and so we should avoid using them in the API? For what it's worth, sum types aren't Rust-specific -- they've been around since at least the 60s, there are established patterns for using them in languages like C, and they appear to have good support in TypeScript, Swift, Scala, and recent Python. That oneOf is part of OpenAPI suggests there's some use for them in this space.
@jessfraz wrote:
why make our users suffer when we could just not do weird shit like a rust-ism like ENUM type
I mean its not that hard either make it an enum of all the same type (strings) or make it a struct where there's a nested enum and the attaching/detaching shit there. The way it is today just creates weird shit for all other languages
why do that when we could just have an object/struct instead
In case it's not clear why folks prefer to use sum types in the API: the reason to use sum types is that they more precisely specify constraints on a value, which facilitates better type checking and so (we hope) a much better user experience. Without sum types, a client needs to do extra validation on top of what the API spec says. In many languages, it's easy to forget to do that and then introduce bugs (e.g., you forget to check if some null-able field is null and then the program crashes at runtime). The extra validation rules also aren't written down anywhere -- there's no way to really know what you need to check. By foregoing this more precise representation in the API spec, we'd be making it easier to introduce bugs not just in languages that don't support these types well, but also the languages that do, including two that we use heavily within the product (Rust and TypeScript).
I like the way @david-crespo said it above:
That makes it a much worse representation in the many languages that do have sum types because you don't know at the type level which states have instances and which don't. So it's not a simple question of what's better for clients: if we make Go much better we make Rust and TS much worse.
and to @smklein's point above, there are good reasons even in Go that someone might prefer the stronger types.
@jessfraz wrote:
The goal is for our customers to not suffer. I do not want to debate this dumb shit, we want idiomatic clients. period. I don't give a shit about rust-ism or not rust-ism but I want to be able to generate idiomatic clients, end of story.
Is it clear what's idiomatic in Go for this situation? From the links in this thread it seems like at least some folks in Go do use sum type patterns -- enough of them that it's even in the Tour of Go.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you were mentioned.
|
In practice when you are actually writing software around this code it’s a lot easier to check if instance is null or None or whatever than to convert the type to the correct type you want, every single time.
Day for example you don’t even care about the instance you just want the state, then it doesn’t even matter for other languages and there’s no extra effort but for go devs they’d still have to convert the type and write all the boilerplate just for that.
Or in the worst case example where you want the instance if it is there it’s only an extra line of code (and maybe a line for the closing bracket depending on the language) to check is instance is null or none.
To me the answer is clear.
… On Jan 3, 2022, at 3:48 PM, Jess Frazelle ***@***.***> wrote:
Honestly it’s the difference between our customers having to type check in Go every time they call a method that returns one of these types versus customers having to across all languages get back an object for disk state with an enum of strings and an optional instance.
It’s like 2 lines of code versus 3*(n # of types) for go. And as an avid user of Go, and how a lot of cloud software and users write go, I would think we would do the former. The trade off will have the most value for our customers.
> On Jan 3, 2022, at 3:45 PM, David Pacheco ***@***.***> wrote:
>
>
> If I'm understanding right, @jessfraz you're saying that the sum types that we're using in the API spec are Rust-isms that are awkward in all other languages and so we should avoid using them in the API? For what it's worth, sum types aren't Rust-specific -- they've been around since at least the 60s, there are established patterns for using them in languages like C, and they appear to have good support in TypeScript, Swift, Scala, and recent Python. That oneOf is part of OpenAPI suggests there's some use for them in this space.
>
> @jessfraz wrote:
>
> why make our users suffer when we could just not do weird shit like a rust-ism like ENUM type
> I mean its not that hard either make it an enum of all the same type (strings) or make it a struct where there's a nested enum and the attaching/detaching shit there. The way it is today just creates weird shit for all other languages
> why do that when we could just have an object/struct instead
>
> In case it's not clear why folks prefer to use sum types in the API: the reason to use sum types is that they more precisely specify constraints on a value, which facilitates better type checking and so (we hope) a much better user experience. Without sum types, a client needs to do extra validation on top of what the API spec says. In many languages, it's easy to forget to do that and then introduce bugs (e.g., you forget to check if some null-able field is null and then the program crashes at runtime). The extra validation rules also aren't written down anywhere -- there's no way to really know what you need to check. By foregoing this more precise representation in the API spec, we'd be making it easier to introduce bugs not just in languages that don't support these types well, but also the languages that do, including two that we use heavily within the product (Rust and TypeScript).
>
> I like the way @david-crespo said it above:
>
> That makes it a much worse representation in the many languages that do have sum types because you don't know at the type level which states have instances and which don't. So it's not a simple question of what's better for clients: if we make Go much better we make Rust and TS much worse.
>
> and to @smklein's point above, there are good reasons even in Go that someone might prefer the stronger types.
>
> @jessfraz wrote:
>
> The goal is for our customers to not suffer. I do not want to debate this dumb shit, we want idiomatic clients. period. I don't give a shit about rust-ism or not rust-ism but I want to be able to generate idiomatic clients, end of story.
>
> Is it clear what's idiomatic in Go for this situation? From the links in this thread it seems like at least some folks in Go do use sum type patterns -- enough of them that it's even in the Tour of Go.
>
> —
> Reply to this email directly, view it on GitHub, or unsubscribe.
> You are receiving this because you were mentioned.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.
|
If it's not acceptable to use the types, then I suspect the client generator could take the union of the properties present in all package main
import "fmt"
/*
* RouteDestination
*/
type RouteDestination struct {
Type string /* variant discriminator */
Ip string /* used by: "ip" */
Name string /* used by: "vpc", "subnet" */
}
/*
* Our consumer:
*/
func main() {
destinations := []RouteDestination{
RouteDestination{Type: "vpc", Name: "blah"},
RouteDestination{Type: "ip", Ip: "127.0.0.1"},
RouteDestination{Type: "subnet", Name: "farend"},
}
for i, rd := range destinations {
fmt.Printf("[ %d ] %+v which is variant type %s\n", i, rd, rd.Type)
switch rd.Type {
case "subnet":
fmt.Printf("\ta subnet, \"%s\", my favourite!\n", rd.Name)
case "ip":
fmt.Printf("\ta direct IP address, %s!\n", rd.Ip)
case "vpc":
fmt.Printf("\ton second thought let's not go to %s, it is a silly place\n", rd.Name)
}
}
} To come up with this, the client generator would look at each variant of the It would be a programming error to produce two variants that include overlapping names with conflicting types, but we could lint for that. @jessfraz, is that more idiomatic? |
I could even make the case for rust that doing a match over all the types is quite annoying:
that's a lot of lines of code when I could get the status back as an enum encapsulated in an object with an optional instance, check if the instance is none |
Sorry, I left out the output:
|
@jclulow thats still a crazy amount of code for something that could be much more simple. |
Can’t you take the flattened approach in the Go client generator based on the oneOf? That would allow other languages to handle it their way. |
No because the end user is the one that suffers and that is not fair |
To be clear, it's just a harness to demonstrate each of the variants. The actual type the generator would produce is: /*
* RouteDestination
*/
type RouteDestination struct {
Type string /* variant discriminator */
Ip string /* used by: "ip" */
Name string /* used by: "vpc", "subnet" */
} You can do whatever you want with it in your consuming code, obviously; e.g., if you only care about subnets: if rd.Type == "subnet" {
/* Do something with rd.Name */
} |
I don’t understand that. I’m saying can’t you generate the client code you want from the oneOf in the spec? i.e., go through the enum variants and build up a single struct with optional fields |
Oh that looks like what I’m talking about @jclulow |
@david-crespo you could if all follow the same pattern, but what if in the future we add a type and it doesn't work in that same way, then we created something gross for end-users, like DiskState is kinda special since it can be expressed as an object with an enum of strings, but how do I know that other than I know that it can be expressed that way whereas it may not be true for all OneOfs |
also @jclulow's example assumes all client code writers know the types off the top of their head, that super sucks, where as just checking if instance is null |
its also non-idomatic as a go dev I'd be like wtf are these guys doing |
I am the first to admit that my Go is not going to win any awards. If you'll forgive my ignorance, can you show me an example of your ideal representation of |
I'd do something like this, which to be honest is different than what I'd do for DiskState, making generating oneOfs very hard, I will show both: (ignore the shitty tabbing I am lazy) Also I'm doing this based on our rust types, which semi sucks because if I wrote the control plane in Go, I'd write the API differently and we'd probably then be in situations (maybe) where the API is too much go-like for generating other languages (I dunno just a hypothesis). Because
|
I also cant seem to find anything that shows me that python will not hit this problem as well |
Typed Python has union types: https://docs.python.org/3/library/typing.html#typing.Union |
Right but to convert it out and get data you need numerous lines of code in like a switch like thing
… On Jan 3, 2022, at 4:51 PM, David Crespo ***@***.***> wrote:
Typed Python has union types: https://docs.python.org/3/library/typing.html#typing.Union
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you were mentioned.
|
I’m honestly just trying to lessen the boilerplate of stuff people have to write just to use our libraries. Because I know when I’m using an api client I’m building something like I’m trying to get somewhere and I want to get there with as little pain / maintenance/ and effort as possible.
… On Jan 3, 2022, at 4:52 PM, Jessie Frazelle ***@***.***> wrote:
Right but to convert it out and get data you need numerous lines of code in like a switch like thing
>> On Jan 3, 2022, at 4:51 PM, David Crespo ***@***.***> wrote:
>>
>
> Typed Python has union types: https://docs.python.org/3/library/typing.html#typing.Union
>
> —
> Reply to this email directly, view it on GitHub, or unsubscribe.
> You are receiving this because you were mentioned.
|
You do, but IMO if you're using Python with types, that would be exactly what you want. If you're using Python without types you can just check if fields are present with |
Crespo we can still have all the generated docs that pop up and tell people stuff, that doesn’t change. The only thing that changes is you don’t get a type error if you call instance after it’s a state that won’t have it, but like you will have a type error to check it’s not null
… On Jan 3, 2022, at 4:54 PM, Jess Frazelle ***@***.***> wrote:
I’m honestly just trying to lessen the boilerplate of stuff people have to write just to use our libraries. Because I know when I’m using an api client I’m building something like I’m trying to get somewhere and I want to get there with as little pain / maintenance/ and effort as possible.
> On Jan 3, 2022, at 4:52 PM, Jessie Frazelle ***@***.***> wrote:
>
>
> Right but to convert it out and get data you need numerous lines of code in like a switch like thing
>
>>> On Jan 3, 2022, at 4:51 PM, David Crespo ***@***.***> wrote:
>>>
>>
>> Typed Python has union types: https://docs.python.org/3/library/typing.html#typing.Union
>>
>> —
>> Reply to this email directly, view it on GitHub, or unsubscribe.
>> You are receiving this because you were mentioned.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.
|
That wasn't just IDE hints, that was the typechecker. As a TS API consumer, if we got rid of the discriminated union, I would personally have to write runtime boilerplate that would at best only partly make up for the loss of type safety. |
@jessfraz I think I understand what you're after. Keeping in mind that the package main
import (
"encoding/json"
"fmt"
)
type DiskStateState string
const (
DiskStateCreating DiskStateState = "creating"
DiskStateDetached DiskStateState = "detached"
DiskStateAttaching DiskStateState = "attaching"
DiskStateAttached DiskStateState = "attached"
DiskStateDetaching DiskStateState = "detaching"
DiskStateDestroyed DiskStateState = "destroyed"
DiskStateFaulted DiskStateState = "faulted"
)
type Instance string
type DiskState struct {
State DiskStateState `json:"state"`
Instance *Instance `json:"instance"`
}
func demo_disk_state() {
jsons := []string{
`{"state":"creating"}`,
`{"state":"attached","instance":"0000001"}`,
`{"state":"detaching","instance":"0000002"}`,
`{"state":"faulted","instance":"0000003"}`,
`{"state":"detached"}`,
}
for i, j := range jsons {
var ds DiskState
json.Unmarshal([]byte(j), &ds)
fmt.Printf("[%02d] %+v\n", i, ds)
if ds.Instance != nil {
fmt.Printf("\tinstance = %v\n", *ds.Instance)
}
if ds.State == DiskStateFaulted {
fmt.Printf("\toh noes!\n")
}
fmt.Printf("\n")
}
}
type RouteDestinationType string
const (
RouteDestinationIP RouteDestinationType = "ip"
RouteDestinationVPC RouteDestinationType = "vpc"
RouteDestinationSubnet RouteDestinationType = "subnet"
)
type RouteDestination struct {
Type RouteDestinationType `json:"type"`
IP string `json:"ip,omitempty"`
Name string `json:"name,omitempty"`
}
func demo_route_destination() {
jsons := []string{
`{"type":"ip","ip":"127.0.0.1"}`,
`{"type":"vpc","name":"camelot"}`,
`{"type":"subnet","name":"omega"}`,
}
for i, j := range jsons {
var rd RouteDestination
json.Unmarshal([]byte(j), &rd)
fmt.Printf("[%02d] %+v\n", i, rd)
fmt.Printf("\n")
}
}
func main() {
fmt.Printf("----------------- Disk State ---------------\n")
demo_disk_state()
fmt.Printf("----------------- Route Destination ---------------\n")
demo_route_destination()
} Outputs:
Demo @ https://go.dev/play/p/g6ZUg8RO4YY If this is acceptable, I think the same basic algorithm could be used to mechanically generate both degenerate structs from the schema that exists today. |
Yes that would work
… On Jan 3, 2022, at 4:58 PM, Joshua M. Clulow ***@***.***> wrote:
@jessfraz I think I understand what you're after. Keeping in mind that the demo_*() functions are just to demonstrate usage, not part of the client, does this look better?
package main
import (
"encoding/json"
"fmt"
)
type DiskStateState string
const (
DiskStateCreating DiskStateState = "creating"
DiskStateDetached DiskStateState = "detached"
DiskStateAttaching DiskStateState = "attaching"
DiskStateAttached DiskStateState = "attached"
DiskStateDetaching DiskStateState = "detaching"
DiskStateDestroyed DiskStateState = "destroyed"
DiskStateFaulted DiskStateState = "faulted"
)
type Instance string
type DiskState struct {
State DiskStateState `json:"state"`
Instance *Instance `json:"instance"`
}
func demo_disk_state() {
jsons := []string{
`{"state":"creating"}`,
`{"state":"attached","instance":"0000001"}`,
`{"state":"detaching","instance":"0000002"}`,
`{"state":"faulted","instance":"0000003"}`,
`{"state":"detached"}`,
}
for i, j := range jsons {
var ds DiskState
json.Unmarshal([]byte(j), &ds)
fmt.Printf("[%02d] %+v\n", i, ds)
if ds.Instance != nil {
fmt.Printf("\tinstance = %v\n", *ds.Instance)
}
if ds.State == DiskStateFaulted {
fmt.Printf("\toh noes!\n")
}
fmt.Printf("\n")
}
}
type RouteDestinationType string
const (
RouteDestinationIP RouteDestinationType = "ip"
RouteDestinationVPC RouteDestinationType = "vpc"
RouteDestinationSubnet RouteDestinationType = "subnet"
)
type RouteDestination struct {
Type RouteDestinationType `json:"type"`
IP string `json:"ip,omitempty"`
Name string `json:"name,omitempty"`
}
func demo_route_destination() {
jsons := []string{
`{"type":"ip","ip":"127.0.0.1"}`,
`{"type":"vpc","name":"camelot"}`,
`{"type":"subnet","name":"omega"}`,
}
for i, j := range jsons {
var rd RouteDestination
json.Unmarshal([]byte(j), &rd)
fmt.Printf("[%02d] %+v\n", i, rd)
fmt.Printf("\n")
}
}
func main() {
fmt.Printf("----------------- Disk State ---------------\n")
demo_disk_state()
fmt.Printf("----------------- Route Destination ---------------\n")
demo_route_destination()
}
Outputs:
----------------- Disk State ---------------
[00] {State:creating Instance:<nil>}
[01] {State:attached Instance:0xc000112170}
instance = 0000001
[02] {State:detaching Instance:0xc0001121a0}
instance = 0000002
[03] {State:faulted Instance:0xc0001121d0}
instance = 0000003
oh noes!
[04] {State:detached Instance:<nil>}
----------------- Route Destination ---------------
[00] {Type:ip IP:127.0.0.1 Name:}
[01] {Type:vpc IP: Name:camelot}
[02] {Type:subnet IP: Name:omega}
Demo @ https://go.dev/play/p/g6ZUg8RO4YY
If this is acceptable, I think the same basic algorithm could be used to mechanically generate both degenerate structs from the schema that exists today.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you were mentioned.
|
Crespo I’m curious what the boiler plate would look like above and beyond a nil check for instance?
… On Jan 3, 2022, at 5:25 PM, Jess Frazelle ***@***.***> wrote:
Yes that would work
> On Jan 3, 2022, at 4:58 PM, Joshua M. Clulow ***@***.***> wrote:
>
>
> @jessfraz I think I understand what you're after. Keeping in mind that the demo_*() functions are just to demonstrate usage, not part of the client, does this look better?
>
> package main
>
> import (
> "encoding/json"
> "fmt"
> )
>
> type DiskStateState string
>
> const (
> DiskStateCreating DiskStateState = "creating"
> DiskStateDetached DiskStateState = "detached"
> DiskStateAttaching DiskStateState = "attaching"
> DiskStateAttached DiskStateState = "attached"
> DiskStateDetaching DiskStateState = "detaching"
> DiskStateDestroyed DiskStateState = "destroyed"
> DiskStateFaulted DiskStateState = "faulted"
> )
>
> type Instance string
>
> type DiskState struct {
> State DiskStateState `json:"state"`
> Instance *Instance `json:"instance"`
> }
>
> func demo_disk_state() {
> jsons := []string{
> `{"state":"creating"}`,
> `{"state":"attached","instance":"0000001"}`,
> `{"state":"detaching","instance":"0000002"}`,
> `{"state":"faulted","instance":"0000003"}`,
> `{"state":"detached"}`,
> }
>
> for i, j := range jsons {
> var ds DiskState
>
> json.Unmarshal([]byte(j), &ds)
>
> fmt.Printf("[%02d] %+v\n", i, ds)
> if ds.Instance != nil {
> fmt.Printf("\tinstance = %v\n", *ds.Instance)
> }
> if ds.State == DiskStateFaulted {
> fmt.Printf("\toh noes!\n")
> }
> fmt.Printf("\n")
> }
> }
>
> type RouteDestinationType string
>
> const (
> RouteDestinationIP RouteDestinationType = "ip"
> RouteDestinationVPC RouteDestinationType = "vpc"
> RouteDestinationSubnet RouteDestinationType = "subnet"
> )
>
> type RouteDestination struct {
> Type RouteDestinationType `json:"type"`
> IP string `json:"ip,omitempty"`
> Name string `json:"name,omitempty"`
> }
>
> func demo_route_destination() {
> jsons := []string{
> `{"type":"ip","ip":"127.0.0.1"}`,
> `{"type":"vpc","name":"camelot"}`,
> `{"type":"subnet","name":"omega"}`,
> }
>
> for i, j := range jsons {
> var rd RouteDestination
>
> json.Unmarshal([]byte(j), &rd)
>
> fmt.Printf("[%02d] %+v\n", i, rd)
> fmt.Printf("\n")
> }
> }
>
> func main() {
> fmt.Printf("----------------- Disk State ---------------\n")
> demo_disk_state()
> fmt.Printf("----------------- Route Destination ---------------\n")
> demo_route_destination()
> }
> Outputs:
>
> ----------------- Disk State ---------------
> [00] {State:creating Instance:<nil>}
>
> [01] {State:attached Instance:0xc000112170}
> instance = 0000001
>
> [02] {State:detaching Instance:0xc0001121a0}
> instance = 0000002
>
> [03] {State:faulted Instance:0xc0001121d0}
> instance = 0000003
> oh noes!
>
> [04] {State:detached Instance:<nil>}
>
> ----------------- Route Destination ---------------
> [00] {Type:ip IP:127.0.0.1 Name:}
>
> [01] {Type:vpc IP: Name:camelot}
>
> [02] {Type:subnet IP: Name:omega}
> Demo @ https://go.dev/play/p/g6ZUg8RO4YY
>
> If this is acceptable, I think the same basic algorithm could be used to mechanically generate both degenerate structs from the schema that exists today.
>
> —
> Reply to this email directly, view it on GitHub, or unsubscribe.
> You are receiving this because you were mentioned.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.
|
This approach is really interesting - the TL;DR of Go Oneof in Proto is: On the one hand, they allow Go clients to follow the "strongly type-safe approach":
But they also add a bunch of stuff that lets you avoid the type cast if you wanna ignore it:
|
In these cases where there's a single nullable field it's not that different, though you might want to do something differently with the For cases where there are more fields you actually save code at runtime by using the discriminant property because you only have to check one thing instead of each property separately. That's on top of typechecker benefits. type SumType =
| {
type: "A";
age: number;
height: number;
}
| {
type: "B";
name: string;
attrs: Record<string, string>;
};
function doSomething(x: SumType) {
if (x.type == "A") {
x.age; // number
x.height; // number
// x.name and x.attrs are type errors
} else {
x.name; // string
x.attrs; // record
// x.age and x.height are type errors
}
} |
I appreciate the summary @smklein — I was not understanding that page very easily. I think that's a cool approach. It reminds me of what you get by default in dynamic languages with optional types, like what I described happening in Python. |
It sounds like our plan here is to retain the precision expressed in the API using |
we had discussed this on: oxidecomputer/dropshot#126
but DiskState, the way it is being parsed creates a one of, so when trying to make a nice idiomatic Go client this super sucks. can we try not do things that are very rust-ism but don't apply nicely in other languages
The text was updated successfully, but these errors were encountered: