Repository: hetznercloud/hcloud-go
Pull request #24 (2018-01-10 08:43:25)

Implement datacenter client


Patch:
diff --git a/hcloud/client.go b/hcloud/client.go
index 5cc8023..c73c294 100644
--- a/hcloud/client.go
+++ b/hcloud/client.go
@@ -52,6 +52,7 @@ type Client struct {
 	httpClient  *http.Client
 
 	Action     ActionClient
+	Datacenter DatacenterClient
 	FloatingIP FloatingIPClient
 	Image      ImageClient
 	Server     ServerClient
@@ -96,6 +97,7 @@ func NewClient(options ...ClientOption) *Client {
 	}
 
 	client.Action = ActionClient{client: client}
+	client.Datacenter = DatacenterClient{client: client}
 	client.FloatingIP = FloatingIPClient{client: client}
 	client.Image = ImageClient{client: client}
 	client.Server = ServerClient{client: client}
diff --git a/hcloud/datacenter.go b/hcloud/datacenter.go
index 5ac4d5a..7b4429f 100644
--- a/hcloud/datacenter.go
+++ b/hcloud/datacenter.go
@@ -1,9 +1,124 @@
 package hcloud
 
+import (
+	"context"
+	"fmt"
+	"net/url"
+	"strconv"
+
+	"github.com/hetznercloud/hcloud-go/hcloud/schema"
+)
+
 // Datacenter represents a datacenter in the Hetzner Cloud.
 type Datacenter struct {
 	ID          int
 	Name        string
 	Description string
 	Location    *Location
+	ServerTypes DatacenterServerTypes
+}
+
+// DatacenterServerTypes represents the server types available and supported in a datacenter.
+type DatacenterServerTypes struct {
+	Supported []*ServerType
+	Available []*ServerType
+}
+
+// DatacenterClient is a client for the datacenter API.
+type DatacenterClient struct {
+	client *Client
+}
+
+// GetByID retrieves a datacenter by its ID.
+func (c *DatacenterClient) GetByID(ctx context.Context, id int) (*Datacenter, *Response, error) {
+	req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/datacenters/%d", id), nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var body schema.DatacenterGetResponse
+	resp, err := c.client.Do(req, &body)
+	if err != nil {
+		if IsError(err, ErrorCodeNotFound) {
+			return nil, resp, nil
+		}
+		return nil, resp, err
+	}
+	return DatacenterFromSchema(body.Datacenter), resp, nil
+}
+
+// GetByName retrieves an datacenter by its name.
+func (c *DatacenterClient) GetByName(ctx context.Context, name string) (*Datacenter, *Response, error) {
+	path := "/datacenters?name=" + url.QueryEscape(name)
+	req, err := c.client.NewRequest(ctx, "GET", path, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var body schema.DatacenterListResponse
+	resp, err := c.client.Do(req, &body)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	if len(body.Datacenters) == 0 {
+		return nil, resp, nil
+	}
+	return DatacenterFromSchema(body.Datacenters[0]), resp, nil
+}
+
+// Get retrieves a datacenter by its ID if the input can be parsed as an integer, otherwise it retrieves a datacenter by its name.
+func (c *DatacenterClient) Get(ctx context.Context, idOrName string) (*Datacenter, *Response, error) {
+	if id, err := strconv.Atoi(idOrName); err == nil {
+		return c.GetByID(ctx, int(id))
+	}
+	return c.GetByName(ctx, idOrName)
+}
+
+// DatacenterListOpts specifies options for listing datacenters.
+type DatacenterListOpts struct {
+	ListOpts
+}
+
+// List returns a list of datacenters for a specific page.
+func (c *DatacenterClient) List(ctx context.Context, opts DatacenterListOpts) ([]*Datacenter, *Response, error) {
+	path := "/datacenters?" + valuesForListOpts(opts.ListOpts).Encode()
+	req, err := c.client.NewRequest(ctx, "GET", path, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var body schema.DatacenterListResponse
+	resp, err := c.client.Do(req, &body)
+	if err != nil {
+		return nil, nil, err
+	}
+	datacenters := make([]*Datacenter, 0, len(body.Datacenters))
+	for _, i := range body.Datacenters {
+		datacenters = append(datacenters, DatacenterFromSchema(i))
+	}
+	return datacenters, resp, nil
+}
+
+// All returns all datacenters.
+func (c *DatacenterClient) All(ctx context.Context) ([]*Datacenter, error) {
+	allDatacenters := []*Datacenter{}
+
+	opts := DatacenterListOpts{}
+	opts.PerPage = 50
+
+	_, err := c.client.all(func(page int) (*Response, error) {
+		opts.Page = page
+		datacenters, resp, err := c.List(ctx, opts)
+		if err != nil {
+			return resp, err
+		}
+		allDatacenters = append(allDatacenters, datacenters...)
+		return resp, nil
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return allDatacenters, nil
 }
diff --git a/hcloud/schema.go b/hcloud/schema.go
index 39f339b..9b6eab8 100644
--- a/hcloud/schema.go
+++ b/hcloud/schema.go
@@ -86,12 +86,23 @@ func LocationFromSchema(s schema.Location) *Location {
 
 // DatacenterFromSchema converts a schema.Datacenter to a Datacenter.
 func DatacenterFromSchema(s schema.Datacenter) *Datacenter {
-	return &Datacenter{
+	d := &Datacenter{
 		ID:          s.ID,
 		Name:        s.Name,
 		Description: s.Description,
 		Location:    LocationFromSchema(s.Location),
+		ServerTypes: DatacenterServerTypes{
+			Available: []*ServerType{},
+			Supported: []*ServerType{},
+		},
 	}
+	for _, t := range s.ServerTypes.Available {
+		d.ServerTypes.Available = append(d.ServerTypes.Available, &ServerType{ID: t})
+	}
+	for _, t := range s.ServerTypes.Supported {
+		d.ServerTypes.Supported = append(d.ServerTypes.Supported, &ServerType{ID: t})
+	}
+	return d
 }
 
 // ServerFromSchema converts a schema.Server to a Server.
diff --git a/hcloud/schema/datacenter.go b/hcloud/schema/datacenter.go
index 0d15080..3e8178e 100644
--- a/hcloud/schema/datacenter.go
+++ b/hcloud/schema/datacenter.go
@@ -6,4 +6,18 @@ type Datacenter struct {
 	Name        string   `json:"name"`
 	Description string   `json:"description"`
 	Location    Location `json:"location"`
+	ServerTypes struct {
+		Supported []int `json:"supported"`
+		Available []int `json:"available"`
+	} `json:"server_types"`
+}
+
+// DatacenterGetResponse defines the schema of the response when retrieving a single datacenter.
+type DatacenterGetResponse struct {
+	Datacenter Datacenter `json:"datacenter"`
+}
+
+// DatacenterListResponse defines the schema of the response when listing datacenters.
+type DatacenterListResponse struct {
+	Datacenters []Datacenter `json:"datacenters"`
 }


Test patch:
diff --git a/hcloud/datacenter_test.go b/hcloud/datacenter_test.go
new file mode 100644
index 0000000..8b98206
--- /dev/null
+++ b/hcloud/datacenter_test.go
@@ -0,0 +1,212 @@
+package hcloud
+
+import (
+	"context"
+	"encoding/json"
+	"net/http"
+	"testing"
+
+	"github.com/hetznercloud/hcloud-go/hcloud/schema"
+)
+
+func TestDatacenterClient(t *testing.T) {
+	t.Run("GetByID", func(t *testing.T) {
+		env := newTestEnv()
+		defer env.Teardown()
+
+		env.Mux.HandleFunc("/datacenters/1", func(w http.ResponseWriter, r *http.Request) {
+			json.NewEncoder(w).Encode(schema.DatacenterGetResponse{
+				Datacenter: schema.Datacenter{
+					ID: 1,
+				},
+			})
+		})
+
+		ctx := context.Background()
+		datacenter, _, err := env.Client.Datacenter.GetByID(ctx, 1)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if datacenter == nil {
+			t.Fatal("no datacenter")
+		}
+		if datacenter.ID != 1 {
+			t.Errorf("unexpected datacenter ID: %v", datacenter.ID)
+		}
+
+		t.Run("via Get", func(t *testing.T) {
+			datacenter, _, err := env.Client.Datacenter.Get(ctx, "1")
+			if err != nil {
+				t.Fatal(err)
+			}
+			if datacenter == nil {
+				t.Fatal("no datacenter")
+			}
+			if datacenter.ID != 1 {
+				t.Errorf("unexpected datacenter ID: %v", datacenter.ID)
+			}
+		})
+	})
+
+	t.Run("GetByID (not found)", func(t *testing.T) {
+		env := newTestEnv()
+		defer env.Teardown()
+
+		env.Mux.HandleFunc("/datacenters/1", func(w http.ResponseWriter, r *http.Request) {
+			w.Header().Set("Content-Type", "application/json")
+			w.WriteHeader(http.StatusNotFound)
+			json.NewEncoder(w).Encode(schema.ErrorResponse{
+				Error: schema.Error{
+					Code: ErrorCodeNotFound,
+				},
+			})
+		})
+
+		ctx := context.Background()
+		datacenter, _, err := env.Client.Datacenter.GetByID(ctx, 1)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if datacenter != nil {
+			t.Fatal("expected no datacenter")
+		}
+	})
+
+	t.Run("GetByName", func(t *testing.T) {
+		env := newTestEnv()
+		defer env.Teardown()
+
+		env.Mux.HandleFunc("/datacenters", func(w http.ResponseWriter, r *http.Request) {
+			if r.URL.RawQuery != "name=fsn1-dc8" {
+				t.Fatal("missing name query")
+			}
+			json.NewEncoder(w).Encode(schema.DatacenterListResponse{
+				Datacenters: []schema.Datacenter{
+					{
+						ID: 1,
+					},
+				},
+			})
+		})
+
+		ctx := context.Background()
+		datacenter, _, err := env.Client.Datacenter.GetByName(ctx, "fsn1-dc8")
+		if err != nil {
+			t.Fatal(err)
+		}
+		if datacenter == nil {
+			t.Fatal("no datacenter")
+		}
+		if datacenter.ID != 1 {
+			t.Errorf("unexpected datacenter ID: %v", datacenter.ID)
+		}
+
+		t.Run("via Get", func(t *testing.T) {
+			datacenter, _, err := env.Client.Datacenter.Get(ctx, "fsn1-dc8")
+			if err != nil {
+				t.Fatal(err)
+			}
+			if datacenter == nil {
+				t.Fatal("no datacenter")
+			}
+			if datacenter.ID != 1 {
+				t.Errorf("unexpected datacenter ID: %v", datacenter.ID)
+			}
+		})
+	})
+
+	t.Run("GetByName (not found)", func(t *testing.T) {
+		env := newTestEnv()
+		defer env.Teardown()
+
+		env.Mux.HandleFunc("/datacenters", func(w http.ResponseWriter, r *http.Request) {
+			if r.URL.RawQuery != "name=fsn1-dc8" {
+				t.Fatal("missing name query")
+			}
+			json.NewEncoder(w).Encode(schema.DatacenterListResponse{
+				Datacenters: []schema.Datacenter{},
+			})
+		})
+
+		ctx := context.Background()
+		datacenter, _, err := env.Client.Datacenter.GetByName(ctx, "fsn1-dc8")
+		if err != nil {
+			t.Fatal(err)
+		}
+		if datacenter != nil {
+			t.Fatal("unexpected datacenter")
+		}
+	})
+
+	t.Run("List", func(t *testing.T) {
+		env := newTestEnv()
+		defer env.Teardown()
+
+		env.Mux.HandleFunc("/datacenters", func(w http.ResponseWriter, r *http.Request) {
+			if page := r.URL.Query().Get("page"); page != "2" {
+				t.Errorf("expected page 2; got %q", page)
+			}
+			if perPage := r.URL.Query().Get("per_page"); perPage != "50" {
+				t.Errorf("expected per_page 50; got %q", perPage)
+			}
+			json.NewEncoder(w).Encode(schema.DatacenterListResponse{
+				Datacenters: []schema.Datacenter{
+					{ID: 1},
+					{ID: 2},
+				},
+			})
+		})
+
+		opts := DatacenterListOpts{}
+		opts.Page = 2
+		opts.PerPage = 50
+
+		ctx := context.Background()
+		datacenters, _, err := env.Client.Datacenter.List(ctx, opts)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if len(datacenters) != 2 {
+			t.Fatal("expected 2 datacenters")
+		}
+	})
+
+	t.Run("All", func(t *testing.T) {
+		env := newTestEnv()
+		defer env.Teardown()
+
+		env.Mux.HandleFunc("/datacenters", func(w http.ResponseWriter, r *http.Request) {
+			w.Header().Set("Content-Type", "application/json")
+			json.NewEncoder(w).Encode(struct {
+				Datacenters []schema.Datacenter `json:"datacenters"`
+				Meta        schema.Meta         `json:"meta"`
+			}{
+				Datacenters: []schema.Datacenter{
+					{ID: 1},
+					{ID: 2},
+					{ID: 3},
+				},
+				Meta: schema.Meta{
+					Pagination: &schema.MetaPagination{
+						Page:         1,
+						LastPage:     1,
+						PerPage:      3,
+						TotalEntries: 3,
+					},
+				},
+			})
+		})
+
+		ctx := context.Background()
+		datacenters, err := env.Client.Datacenter.All(ctx)
+		if err != nil {
+			t.Fatalf("Datacenter.List failed: %s", err)
+		}
+		if len(datacenters) != 3 {
+			t.Fatalf("expected 3 datacenters; got %d", len(datacenters))
+		}
+		if datacenters[0].ID != 1 || datacenters[1].ID != 2 || datacenters[2].ID != 3 {
+			t.Errorf("unexpected datacenters")
+		}
+	})
+}
diff --git a/hcloud/schema_test.go b/hcloud/schema_test.go
index fcefa5f..322d751 100644
--- a/hcloud/schema_test.go
+++ b/hcloud/schema_test.go
@@ -251,6 +251,12 @@ func TestDatacenterFromSchema(t *testing.T) {
 	if datacenter.Location == nil || datacenter.Location.ID != 1 {
 		t.Errorf("unexpected Location: %v", datacenter.Location)
 	}
+	if len(datacenter.ServerTypes.Available) != 4 {
+		t.Errorf("unexpected ServerTypes.Available (should be 4): %v", len(datacenter.ServerTypes.Available))
+	}
+	if len(datacenter.ServerTypes.Supported) != 4 {
+		t.Errorf("unexpected ServerTypes.Supported length (should be 4): %v", len(datacenter.ServerTypes.Supported))
+	}
 }
 
 func TestServerFromSchema(t *testing.T) {