diff --git a/hcloud/load_balancer.go b/hcloud/load_balancer.go index fb40057b..c634d160 100644 --- a/hcloud/load_balancer.go +++ b/hcloud/load_balancer.go @@ -935,10 +935,8 @@ type LoadBalancerChangeTypeOpts struct { // ChangeType changes a Load Balancer's type. func (c *LoadBalancerClient) ChangeType(ctx context.Context, loadBalancer *LoadBalancer, opts LoadBalancerChangeTypeOpts) (*Action, *Response, error) { reqBody := schema.LoadBalancerActionChangeTypeRequest{} - if opts.LoadBalancerType.ID != 0 { - reqBody.LoadBalancerType = opts.LoadBalancerType.ID - } else { - reqBody.LoadBalancerType = opts.LoadBalancerType.Name + if opts.LoadBalancerType.ID != 0 || opts.LoadBalancerType.Name != "" { + reqBody.LoadBalancerType = schema.IDOrName{ID: opts.LoadBalancerType.ID, Name: opts.LoadBalancerType.Name} } reqBodyData, err := json.Marshal(reqBody) if err != nil { diff --git a/hcloud/load_balancer_test.go b/hcloud/load_balancer_test.go index f9190b26..2f18efe3 100644 --- a/hcloud/load_balancer_test.go +++ b/hcloud/load_balancer_test.go @@ -175,7 +175,7 @@ func TestLoadBalancerCreate(t *testing.T) { } expectedReqBody := schema.LoadBalancerCreateRequest{ Name: "load-balancer", - LoadBalancerType: "lb1", + LoadBalancerType: schema.IDOrName{Name: "lb1"}, Algorithm: &schema.LoadBalancerCreateRequestAlgorithm{ Type: "round_robin", }, @@ -813,7 +813,7 @@ func TestLoadBalancerClientChangeType(t *testing.T) { if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } - if id, ok := reqBody.LoadBalancerType.(float64); !ok || id != 1 { + if reqBody.LoadBalancerType.ID != 1 { t.Errorf("unexpected Load Balancer type ID: %v", reqBody.LoadBalancerType) } json.NewEncoder(w).Encode(schema.LoadBalancerActionChangeTypeResponse{ @@ -844,7 +844,7 @@ func TestLoadBalancerClientChangeType(t *testing.T) { if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } - if name, ok := reqBody.LoadBalancerType.(string); !ok || name != "type" { + if reqBody.LoadBalancerType.Name != "type" { t.Errorf("unexpected Load Balancer type name: %v", reqBody.LoadBalancerType) } json.NewEncoder(w).Encode(schema.LoadBalancerActionChangeTypeResponse{ diff --git a/hcloud/schema/id_or_name.go b/hcloud/schema/id_or_name.go new file mode 100644 index 00000000..d7de73cc --- /dev/null +++ b/hcloud/schema/id_or_name.go @@ -0,0 +1,67 @@ +package schema + +import ( + "bytes" + "encoding/json" + "reflect" + "strconv" +) + +// IDOrName can be used in API requests where either a resource id or name can be +// specified. +type IDOrName struct { + ID int64 + Name string +} + +var _ json.Unmarshaler = (*IDOrName)(nil) +var _ json.Marshaler = (*IDOrName)(nil) + +func (o IDOrName) MarshalJSON() ([]byte, error) { + if o.ID != 0 { + return json.Marshal(o.ID) + } + if o.Name != "" { + return json.Marshal(o.Name) + } + return nil, &json.UnsupportedValueError{ + Value: reflect.ValueOf(o), + Str: "id or name must not be zero values", + } +} + +func (o *IDOrName) UnmarshalJSON(data []byte) error { + d := json.NewDecoder(bytes.NewBuffer(data)) + d.UseNumber() // This ensures we won't lose precision on large IDs + + var v any + if err := d.Decode(&v); err != nil { + return err + } + + switch typed := v.(type) { + case string: + id, err := strconv.ParseInt(typed, 10, 64) + if err == nil { + o.ID = id + } else if typed != "" { + o.Name = typed + } + case json.Number: + id, err := typed.Int64() + if err != nil { + return &json.UnmarshalTypeError{ + Value: "number", + Type: reflect.TypeOf(*o), + } + } + o.ID = id + default: + return &json.UnmarshalTypeError{ + Value: reflect.TypeOf(typed).Name(), + Type: reflect.TypeOf(*o), + } + } + + return nil +} diff --git a/hcloud/schema/id_or_name_test.go b/hcloud/schema/id_or_name_test.go new file mode 100644 index 00000000..78d67eff --- /dev/null +++ b/hcloud/schema/id_or_name_test.go @@ -0,0 +1,66 @@ +package schema + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIDOrNameMarshall(t *testing.T) { + t.Run("id", func(t *testing.T) { + i := IDOrName{ID: 1} + + got, err := i.MarshalJSON() + require.NoError(t, err) + require.Equal(t, `1`, string(got)) + }) + + t.Run("name", func(t *testing.T) { + i := IDOrName{Name: "name"} + + got, err := i.MarshalJSON() + require.NoError(t, err) + require.Equal(t, `"name"`, string(got)) + }) + + t.Run("id and name", func(t *testing.T) { + i := IDOrName{ID: 1, Name: "name"} + + got, err := i.MarshalJSON() + require.NoError(t, err) + require.Equal(t, `1`, string(got)) + }) + + t.Run("none", func(t *testing.T) { + i := IDOrName{} + + _, err := i.MarshalJSON() + require.EqualError(t, err, "json: unsupported value: id or name must not be zero values") + }) +} + +func TestIDOrNameUnMarshall(t *testing.T) { + t.Run("id", func(t *testing.T) { + i := IDOrName{} + + err := i.UnmarshalJSON([]byte(`1`)) + require.NoError(t, err) + require.Equal(t, IDOrName{ID: 1}, i) + }) + + t.Run("name", func(t *testing.T) { + i := IDOrName{} + + err := i.UnmarshalJSON([]byte(`"name"`)) + require.NoError(t, err) + require.Equal(t, IDOrName{Name: "name"}, i) + }) + + t.Run("id string", func(t *testing.T) { + i := IDOrName{} + + err := i.UnmarshalJSON([]byte(`"1"`)) + require.NoError(t, err) + require.Equal(t, IDOrName{ID: 1}, i) + }) +} diff --git a/hcloud/schema/load_balancer.go b/hcloud/schema/load_balancer.go index 7e1c4f5d..d8760ad2 100644 --- a/hcloud/schema/load_balancer.go +++ b/hcloud/schema/load_balancer.go @@ -251,7 +251,7 @@ type LoadBalancerDeleteServiceResponse struct { type LoadBalancerCreateRequest struct { Name string `json:"name"` - LoadBalancerType interface{} `json:"load_balancer_type"` // int or string + LoadBalancerType IDOrName `json:"load_balancer_type"` Algorithm *LoadBalancerCreateRequestAlgorithm `json:"algorithm,omitempty"` Location *string `json:"location,omitempty"` NetworkZone *string `json:"network_zone,omitempty"` @@ -380,7 +380,7 @@ type LoadBalancerActionDisablePublicInterfaceResponse struct { } type LoadBalancerActionChangeTypeRequest struct { - LoadBalancerType interface{} `json:"load_balancer_type"` // int or string + LoadBalancerType IDOrName `json:"load_balancer_type"` } type LoadBalancerActionChangeTypeResponse struct { diff --git a/hcloud/schema/server.go b/hcloud/schema/server.go index b9c945a8..72aaf269 100644 --- a/hcloud/schema/server.go +++ b/hcloud/schema/server.go @@ -99,8 +99,8 @@ type ServerListResponse struct { // create a server. type ServerCreateRequest struct { Name string `json:"name"` - ServerType interface{} `json:"server_type"` // int or string - Image interface{} `json:"image"` // int or string + ServerType IDOrName `json:"server_type"` + Image IDOrName `json:"image"` SSHKeys []int64 `json:"ssh_keys,omitempty"` Location string `json:"location,omitempty"` Datacenter string `json:"datacenter,omitempty"` @@ -257,7 +257,7 @@ type ServerActionDisableRescueResponse struct { // ServerActionRebuildRequest defines the schema for the request to // rebuild a server. type ServerActionRebuildRequest struct { - Image interface{} `json:"image"` // int or string + Image IDOrName `json:"image"` } // ServerActionRebuildResponse defines the schema of the response when @@ -270,7 +270,7 @@ type ServerActionRebuildResponse struct { // ServerActionAttachISORequest defines the schema for the request to // attach an ISO to a server. type ServerActionAttachISORequest struct { - ISO interface{} `json:"iso"` // int or string + ISO IDOrName `json:"iso"` } // ServerActionAttachISOResponse defines the schema of the response when @@ -308,8 +308,8 @@ type ServerActionDisableBackupResponse struct { // ServerActionChangeTypeRequest defines the schema for the request to // change a server's type. type ServerActionChangeTypeRequest struct { - ServerType interface{} `json:"server_type"` // int or string - UpgradeDisk bool `json:"upgrade_disk"` + ServerType IDOrName `json:"server_type"` + UpgradeDisk bool `json:"upgrade_disk"` } // ServerActionChangeTypeResponse defines the schema of the response when diff --git a/hcloud/schema/volume.go b/hcloud/schema/volume.go index 1de64595..89223ba4 100644 --- a/hcloud/schema/volume.go +++ b/hcloud/schema/volume.go @@ -23,7 +23,7 @@ type VolumeCreateRequest struct { Name string `json:"name"` Size int `json:"size"` Server *int64 `json:"server,omitempty"` - Location interface{} `json:"location,omitempty"` // int, string, or nil + Location *IDOrName `json:"location,omitempty"` Labels *map[string]string `json:"labels,omitempty"` Automount *bool `json:"automount,omitempty"` Format *string `json:"format,omitempty"` diff --git a/hcloud/schema_gen.go b/hcloud/schema_gen.go index c2c37351..4566acfe 100644 --- a/hcloud/schema_gen.go +++ b/hcloud/schema_gen.go @@ -69,7 +69,6 @@ You can find a documentation of goverter here: https://goverter.jmattheis.de/ // goverter:extend durationFromIntSeconds // goverter:extend intSecondsFromDuration // goverter:extend serverFromImageCreatedFromSchema -// goverter:extend anyFromLoadBalancerType // goverter:extend serverMetricsTimeSeriesFromSchema // goverter:extend loadBalancerMetricsTimeSeriesFromSchema // goverter:extend stringPtrFromLoadBalancerServiceProtocol @@ -808,16 +807,6 @@ func volumePricingFromSchema(s schema.Pricing) VolumePricing { } } -func anyFromLoadBalancerType(t *LoadBalancerType) interface{} { - if t == nil { - return nil - } - if t.ID != 0 { - return t.ID - } - return t.Name -} - func serverMetricsTimeSeriesFromSchema(s schema.ServerTimeSeriesVals) ([]ServerMetricsValue, error) { vals := make([]ServerMetricsValue, len(s.Values)) diff --git a/hcloud/schema_test.go b/hcloud/schema_test.go index 3ee2779e..3bec207f 100644 --- a/hcloud/schema_test.go +++ b/hcloud/schema_test.go @@ -1508,7 +1508,7 @@ func TestLoadBalancerCreateOptsToSchema(t *testing.T) { }, Request: schema.LoadBalancerCreateRequest{ Name: "test", - LoadBalancerType: "lb11", + LoadBalancerType: schema.IDOrName{Name: "lb11"}, Algorithm: &schema.LoadBalancerCreateRequestAlgorithm{ Type: string(LoadBalancerAlgorithmTypeRoundRobin), }, @@ -1593,7 +1593,7 @@ func TestLoadBalancerCreateOptsToSchema(t *testing.T) { }, Request: schema.LoadBalancerCreateRequest{ Name: "test", - LoadBalancerType: "lb11", + LoadBalancerType: schema.IDOrName{Name: "lb11"}, Algorithm: &schema.LoadBalancerCreateRequestAlgorithm{ Type: string(LoadBalancerAlgorithmTypeRoundRobin), }, diff --git a/hcloud/server.go b/hcloud/server.go index 11857e17..d1514f5c 100644 --- a/hcloud/server.go +++ b/hcloud/server.go @@ -378,15 +378,11 @@ func (c *ServerClient) Create(ctx context.Context, opts ServerCreateOpts) (Serve reqBody.Name = opts.Name reqBody.Automount = opts.Automount reqBody.StartAfterCreate = opts.StartAfterCreate - if opts.ServerType.ID != 0 { - reqBody.ServerType = opts.ServerType.ID - } else if opts.ServerType.Name != "" { - reqBody.ServerType = opts.ServerType.Name + if opts.ServerType.ID != 0 || opts.ServerType.Name != "" { + reqBody.ServerType = schema.IDOrName{ID: opts.ServerType.ID, Name: opts.ServerType.Name} } - if opts.Image.ID != 0 { - reqBody.Image = opts.Image.ID - } else if opts.Image.Name != "" { - reqBody.Image = opts.Image.Name + if opts.Image.ID != 0 || opts.Image.Name != "" { + reqBody.Image = schema.IDOrName{ID: opts.Image.ID, Name: opts.Image.Name} } if opts.Labels != nil { reqBody.Labels = &opts.Labels @@ -778,10 +774,8 @@ func (c *ServerClient) Rebuild(ctx context.Context, server *Server, opts ServerR // RebuildWithResult rebuilds a server. func (c *ServerClient) RebuildWithResult(ctx context.Context, server *Server, opts ServerRebuildOpts) (ServerRebuildResult, *Response, error) { reqBody := schema.ServerActionRebuildRequest{} - if opts.Image.ID != 0 { - reqBody.Image = opts.Image.ID - } else { - reqBody.Image = opts.Image.Name + if opts.Image.ID != 0 || opts.Image.Name != "" { + reqBody.Image = schema.IDOrName{ID: opts.Image.ID, Name: opts.Image.Name} } reqBodyData, err := json.Marshal(reqBody) if err != nil { @@ -813,10 +807,8 @@ func (c *ServerClient) RebuildWithResult(ctx context.Context, server *Server, op // AttachISO attaches an ISO to a server. func (c *ServerClient) AttachISO(ctx context.Context, server *Server, iso *ISO) (*Action, *Response, error) { reqBody := schema.ServerActionAttachISORequest{} - if iso.ID != 0 { - reqBody.ISO = iso.ID - } else { - reqBody.ISO = iso.Name + if iso.ID != 0 || iso.Name != "" { + reqBody.ISO = schema.IDOrName{ID: iso.ID, Name: iso.Name} } reqBodyData, err := json.Marshal(reqBody) if err != nil { @@ -899,10 +891,8 @@ func (c *ServerClient) ChangeType(ctx context.Context, server *Server, opts Serv reqBody := schema.ServerActionChangeTypeRequest{ UpgradeDisk: opts.UpgradeDisk, } - if opts.ServerType.ID != 0 { - reqBody.ServerType = opts.ServerType.ID - } else { - reqBody.ServerType = opts.ServerType.Name + if opts.ServerType.ID != 0 || opts.ServerType.Name != "" { + reqBody.ServerType = schema.IDOrName{ID: opts.ServerType.ID, Name: opts.ServerType.Name} } reqBodyData, err := json.Marshal(reqBody) if err != nil { diff --git a/hcloud/server_test.go b/hcloud/server_test.go index 0a015135..f1e4535a 100644 --- a/hcloud/server_test.go +++ b/hcloud/server_test.go @@ -1468,7 +1468,7 @@ func TestServerClientRebuild(t *testing.T) { if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } - if id, ok := reqBody.Image.(float64); !ok || id != 1 { + if reqBody.Image.ID != 1 { t.Errorf("unexpected image ID: %v", reqBody.Image) } json.NewEncoder(w).Encode(schema.ServerActionRebuildResponse{ @@ -1499,7 +1499,7 @@ func TestServerClientRebuild(t *testing.T) { if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } - if name, ok := reqBody.Image.(string); !ok || name != "debian-9" { + if reqBody.Image.Name != "debian-9" { t.Errorf("unexpected image name: %v", reqBody.Image) } json.NewEncoder(w).Encode(schema.ServerActionRebuildResponse{ @@ -1537,7 +1537,7 @@ func TestServerClientRebuildWithResult(t *testing.T) { if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } - if id, ok := reqBody.Image.(float64); !ok || id != 1 { + if reqBody.Image.ID != 1 { t.Errorf("unexpected image ID: %v", reqBody.Image) } json.NewEncoder(w).Encode(schema.ServerActionRebuildResponse{ @@ -1572,7 +1572,7 @@ func TestServerClientRebuildWithResult(t *testing.T) { if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } - if name, ok := reqBody.Image.(string); !ok || name != "debian-9" { + if reqBody.Image.Name != "debian-9" { t.Errorf("unexpected image name: %v", reqBody.Image) } json.NewEncoder(w).Encode(schema.ServerActionRebuildResponse{ @@ -1614,7 +1614,7 @@ func TestServerClientAttachISO(t *testing.T) { if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } - if id, ok := reqBody.ISO.(float64); !ok || id != 1 { + if reqBody.ISO.ID != 1 { t.Errorf("unexpected ISO ID: %v", reqBody.ISO) } json.NewEncoder(w).Encode(schema.ServerActionAttachISOResponse{ @@ -1643,7 +1643,7 @@ func TestServerClientAttachISO(t *testing.T) { if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } - if name, ok := reqBody.ISO.(string); !ok || name != "debian.iso" { + if reqBody.ISO.Name != "debian.iso" { t.Errorf("unexpected ISO name: %v", reqBody.ISO) } json.NewEncoder(w).Encode(schema.ServerActionAttachISOResponse{ @@ -1757,7 +1757,7 @@ func TestServerClientChangeType(t *testing.T) { if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } - if id, ok := reqBody.ServerType.(float64); !ok || id != 1 { + if reqBody.ServerType.ID != 1 { t.Errorf("unexpected server type ID: %v", reqBody.ServerType) } if !reqBody.UpgradeDisk { @@ -1792,7 +1792,7 @@ func TestServerClientChangeType(t *testing.T) { if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { t.Fatal(err) } - if name, ok := reqBody.ServerType.(string); !ok || name != "type" { + if reqBody.ServerType.Name != "type" { t.Errorf("unexpected server type name: %v", reqBody.ServerType) } if !reqBody.UpgradeDisk { diff --git a/hcloud/volume.go b/hcloud/volume.go index c744b5a8..6fe9d5ab 100644 --- a/hcloud/volume.go +++ b/hcloud/volume.go @@ -220,10 +220,8 @@ func (c *VolumeClient) Create(ctx context.Context, opts VolumeCreateOpts) (Volum reqBody.Server = Ptr(opts.Server.ID) } if opts.Location != nil { - if opts.Location.ID != 0 { - reqBody.Location = opts.Location.ID - } else { - reqBody.Location = opts.Location.Name + if opts.Location.ID != 0 || opts.Location.Name != "" { + reqBody.Location = &schema.IDOrName{ID: opts.Location.ID, Name: opts.Location.Name} } } diff --git a/hcloud/volume_test.go b/hcloud/volume_test.go index 8e2255a3..de05b945 100644 --- a/hcloud/volume_test.go +++ b/hcloud/volume_test.go @@ -319,7 +319,7 @@ func TestVolumeClientCreateWithLocation(t *testing.T) { if reqBody.Size != 42 { t.Errorf("unexpected volume size in request: %v", reqBody.Size) } - if reqBody.Location != float64(1) { + if reqBody.Location.ID != 1 { t.Errorf("unexpected volume location in request: %v", reqBody.Location) } if reqBody.Server != nil { diff --git a/hcloud/zz_schema.go b/hcloud/zz_schema.go old mode 100755 new mode 100644 index c739e1d8..4f4b866f --- a/hcloud/zz_schema.go +++ b/hcloud/zz_schema.go @@ -690,7 +690,7 @@ func (c *converterImpl) SchemaFromLoadBalancerAddServiceOpts(source LoadBalancer func (c *converterImpl) SchemaFromLoadBalancerCreateOpts(source LoadBalancerCreateOpts) schema.LoadBalancerCreateRequest { var schemaLoadBalancerCreateRequest schema.LoadBalancerCreateRequest schemaLoadBalancerCreateRequest.Name = source.Name - schemaLoadBalancerCreateRequest.LoadBalancerType = anyFromLoadBalancerType(source.LoadBalancerType) + schemaLoadBalancerCreateRequest.LoadBalancerType = c.pHcloudLoadBalancerTypeToSchemaIDOrName(source.LoadBalancerType) schemaLoadBalancerCreateRequest.Algorithm = c.pHcloudLoadBalancerAlgorithmToPSchemaLoadBalancerCreateRequestAlgorithm(source.Algorithm) schemaLoadBalancerCreateRequest.Location = c.pHcloudLocationToPString(source.Location) schemaLoadBalancerCreateRequest.NetworkZone = stringPtrFromNetworkZone(source.NetworkZone) @@ -1732,6 +1732,16 @@ func (c *converterImpl) pHcloudLoadBalancerToInt64(source *LoadBalancer) int64 { } return xint64 } +func (c *converterImpl) pHcloudLoadBalancerTypeToSchemaIDOrName(source *LoadBalancerType) schema.IDOrName { + var schemaIDOrName schema.IDOrName + if source != nil { + var schemaIDOrName2 schema.IDOrName + schemaIDOrName2.ID = (*source).ID + schemaIDOrName2.Name = (*source).Name + schemaIDOrName = schemaIDOrName2 + } + return schemaIDOrName +} func (c *converterImpl) pHcloudLoadBalancerUpdateServiceOptsHTTPToPSchemaLoadBalancerActionUpdateServiceRequestHTTP(source *LoadBalancerUpdateServiceOptsHTTP) *schema.LoadBalancerActionUpdateServiceRequestHTTP { var pSchemaLoadBalancerActionUpdateServiceRequestHTTP *schema.LoadBalancerActionUpdateServiceRequestHTTP if source != nil {