Repository: hetznercloud/hcloud-go (go)

Issue:
Implement getting pricing information
https://docs.hetzner.cloud/#resources-pricing-get

Pull request description:
Implement retrieving pricing information
Closes #38.

Patch:
diff --git a/hcloud/client.go b/hcloud/client.go
index 1bfc7a3..220f9c4 100644
--- a/hcloud/client.go
+++ b/hcloud/client.go
@@ -57,6 +57,7 @@ type Client struct {
 	Image      ImageClient
 	ISO        ISOClient
 	Location   LocationClient
+	Pricing    PricingClient
 	Server     ServerClient
 	ServerType ServerTypeClient
 	SSHKey     SSHKeyClient
@@ -104,6 +105,7 @@ func NewClient(options ...ClientOption) *Client {
 	client.Image = ImageClient{client: client}
 	client.ISO = ISOClient{client: client}
 	client.Location = LocationClient{client: client}
+	client.Pricing = PricingClient{client: client}
 	client.Server = ServerClient{client: client}
 	client.ServerType = ServerTypeClient{client: client}
 	client.SSHKey = SSHKeyClient{client: client}
diff --git a/hcloud/pricing.go b/hcloud/pricing.go
new file mode 100644
index 0000000..3bf7ec4
--- /dev/null
+++ b/hcloud/pricing.go
@@ -0,0 +1,80 @@
+package hcloud
+
+import (
+	"context"
+
+	"github.com/hetznercloud/hcloud-go/hcloud/schema"
+)
+
+// Pricing specifies pricing information for various resources.
+type Pricing struct {
+	Image        ImagePricing
+	FloatingIP   FloatingIPPricing
+	Traffic      TrafficPricing
+	ServerBackup ServerBackupPricing
+	ServerTypes  []ServerTypePricing
+}
+
+// Price represents a price. Net amount, gross amount, as well as VAT rate are
+// specified as strings and it is the user's responsibility to convert them to
+// appropriate types for calculations.
+type Price struct {
+	Currency string
+	VATRate  string
+	Net      string
+	Gross    string
+}
+
+// ImagePricing provides pricing information for imaegs.
+type ImagePricing struct {
+	PerGBMonth Price
+}
+
+// FloatingIPPricing provides pricing information for Floating IPs.
+type FloatingIPPricing struct {
+	Monthly Price
+}
+
+// TrafficPricing provides pricing information for traffic.
+type TrafficPricing struct {
+	PerGB Price
+}
+
+// ServerBackupPricing provides pricing information for server backups.
+type ServerBackupPricing struct {
+	Percentage string
+}
+
+// ServerTypePricing provides pricing information for a server type.
+type ServerTypePricing struct {
+	ServerType *ServerType
+	Pricings   []ServerTypeLocationPricing
+}
+
+// ServerTypeLocationPricing provides pricing information for a server type
+// at a location.
+type ServerTypeLocationPricing struct {
+	Location *Location
+	Hourly   Price
+	Monthly  Price
+}
+
+// PricingClient is a client for the pricing API.
+type PricingClient struct {
+	client *Client
+}
+
+// Get retrieves pricing information.
+func (c *PricingClient) Get(ctx context.Context) (Pricing, *Response, error) {
+	req, err := c.client.NewRequest(ctx, "GET", "/pricing", nil)
+	if err != nil {
+		return Pricing{}, nil, err
+	}
+
+	var body schema.PricingGetResponse
+	resp, err := c.client.Do(req, &body)
+	if err != nil {
+		return Pricing{}, nil, err
+	}
+	return PricingFromSchema(body.Pricing), resp, nil
+}
diff --git a/hcloud/schema.go b/hcloud/schema.go
index 9b6eab8..69dbf75 100644
--- a/hcloud/schema.go
+++ b/hcloud/schema.go
@@ -182,7 +182,19 @@ func ServerTypeFromSchema(s schema.ServerType) *ServerType {
 		Disk:        s.Disk,
 		StorageType: StorageType(s.StorageType),
 	}
-
+	for _, price := range s.Prices {
+		st.Pricings = append(st.Pricings, ServerTypeLocationPricing{
+			Location: &Location{Name: price.Location},
+			Hourly: Price{
+				Net:   price.PriceHourly.Net,
+				Gross: price.PriceHourly.Gross,
+			},
+			Monthly: Price{
+				Net:   price.PriceMonthly.Net,
+				Gross: price.PriceMonthly.Gross,
+			},
+		})
+	}
 	return st
 }
 
@@ -265,3 +277,64 @@ func ErrorFromSchema(s schema.Error) Error {
 	}
 	return e
 }
+
+// PricingFromSchema converts a schema.Pricing to a Pricing.
+func PricingFromSchema(s schema.Pricing) Pricing {
+	p := Pricing{
+		Image: ImagePricing{
+			PerGBMonth: Price{
+				Currency: s.Currency,
+				VATRate:  s.VATRate,
+				Net:      s.Image.PricePerGBMonth.Net,
+				Gross:    s.Image.PricePerGBMonth.Gross,
+			},
+		},
+		FloatingIP: FloatingIPPricing{
+			Monthly: Price{
+				Currency: s.Currency,
+				VATRate:  s.VATRate,
+				Net:      s.FloatingIP.PriceMonthly.Net,
+				Gross:    s.FloatingIP.PriceMonthly.Gross,
+			},
+		},
+		Traffic: TrafficPricing{
+			PerGB: Price{
+				Currency: s.Currency,
+				VATRate:  s.VATRate,
+				Net:      s.Traffic.PricePerGB.Net,
+				Gross:    s.Traffic.PricePerGB.Gross,
+			},
+		},
+		ServerBackup: ServerBackupPricing{
+			Percentage: s.ServerBackup.Percentage,
+		},
+	}
+	for _, serverType := range s.ServerTypes {
+		var pricings []ServerTypeLocationPricing
+		for _, price := range serverType.Prices {
+			pricings = append(pricings, ServerTypeLocationPricing{
+				Location: &Location{Name: price.Location},
+				Hourly: Price{
+					Currency: s.Currency,
+					VATRate:  s.VATRate,
+					Net:      price.PriceHourly.Net,
+					Gross:    price.PriceHourly.Gross,
+				},
+				Monthly: Price{
+					Currency: s.Currency,
+					VATRate:  s.VATRate,
+					Net:      price.PriceMonthly.Net,
+					Gross:    price.PriceMonthly.Gross,
+				},
+			})
+		}
+		p.ServerTypes = append(p.ServerTypes, ServerTypePricing{
+			ServerType: &ServerType{
+				ID:   serverType.ID,
+				Name: serverType.Name,
+			},
+			Pricings: pricings,
+		})
+	}
+	return p
+}
diff --git a/hcloud/schema/pricing.go b/hcloud/schema/pricing.go
new file mode 100644
index 0000000..4bf9e07
--- /dev/null
+++ b/hcloud/schema/pricing.go
@@ -0,0 +1,58 @@
+package schema
+
+// Pricing defines the schema for pricing information.
+type Pricing struct {
+	Currency     string              `json:"currency"`
+	VATRate      string              `json:"vat_rate"`
+	Image        PricingImage        `json:"image"`
+	FloatingIP   PricingFloatingIP   `json:"floating_ip"`
+	Traffic      PricingTraffic      `json:"traffic"`
+	ServerBackup PricingServerBackup `json:"server_backup"`
+	ServerTypes  []PricingServerType `json:"server_types"`
+}
+
+// Price defines the schema of a single price with net and gross amount.
+type Price struct {
+	Net   string `json:"net"`
+	Gross string `json:"gross"`
+}
+
+// PricingImage defines the schema of pricing information for an image.
+type PricingImage struct {
+	PricePerGBMonth Price `json:"price_per_gb_month"`
+}
+
+// PricingFloatingIP defines the schema of pricing information for a Floating IP.
+type PricingFloatingIP struct {
+	PriceMonthly Price `json:"price_monthly"`
+}
+
+// PricingTraffic defines the schema of pricing information for traffic.
+type PricingTraffic struct {
+	PricePerGB Price `json:"price_per_gb"`
+}
+
+// PricingServerBackup defines the schema of pricing information for server backups.
+type PricingServerBackup struct {
+	Percentage string `json:"percentage"`
+}
+
+// PricingServerType defines the schema of pricing information for a server type.
+type PricingServerType struct {
+	ID     int                      `json:"id"`
+	Name   string                   `json:"name"`
+	Prices []PricingServerTypePrice `json:"prices"`
+}
+
+// PricingServerTypePrice defines the schema of pricing information for a server
+// type at a location.
+type PricingServerTypePrice struct {
+	Location     string `json:"location"`
+	PriceHourly  Price  `json:"price_hourly"`
+	PriceMonthly Price  `json:"price_monthly"`
+}
+
+// PricingGetResponse defines the schema of the response when retrieving pricing information.
+type PricingGetResponse struct {
+	Pricing Pricing `json:"pricing"`
+}
diff --git a/hcloud/schema/server_type.go b/hcloud/schema/server_type.go
index 8441a0d..272726f 100644
--- a/hcloud/schema/server_type.go
+++ b/hcloud/schema/server_type.go
@@ -2,13 +2,14 @@ package schema
 
 // ServerType defines the schema of a server type.
 type ServerType struct {
-	ID          int     `json:"id"`
-	Name        string  `json:"name"`
-	Description string  `json:"description"`
-	Cores       int     `json:"cores"`
-	Memory      float32 `json:"memory"`
-	Disk        int     `json:"disk"`
-	StorageType string  `json:"storage_type"`
+	ID          int                      `json:"id"`
+	Name        string                   `json:"name"`
+	Description string                   `json:"description"`
+	Cores       int                      `json:"cores"`
+	Memory      float32                  `json:"memory"`
+	Disk        int                      `json:"disk"`
+	StorageType string                   `json:"storage_type"`
+	Prices      []PricingServerTypePrice `json:"prices"`
 }
 
 // ServerTypeListResponse defines the schema of the response when
diff --git a/hcloud/server_type.go b/hcloud/server_type.go
index d680e9a..2db9f50 100644
--- a/hcloud/server_type.go
+++ b/hcloud/server_type.go
@@ -18,6 +18,7 @@ type ServerType struct {
 	Memory      float32
 	Disk        int
 	StorageType StorageType
+	Pricings    []ServerTypeLocationPricing
 }
 
 // StorageType specifies the type of storage.


Test patch:
diff --git a/hcloud/pricing_test.go b/hcloud/pricing_test.go
new file mode 100644
index 0000000..f85ee99
--- /dev/null
+++ b/hcloud/pricing_test.go
@@ -0,0 +1,32 @@
+package hcloud
+
+import (
+	"context"
+	"encoding/json"
+	"net/http"
+	"testing"
+
+	"github.com/hetznercloud/hcloud-go/hcloud/schema"
+)
+
+func TestPricingClientGet(t *testing.T) {
+	env := newTestEnv()
+	defer env.Teardown()
+
+	env.Mux.HandleFunc("/pricing", func(w http.ResponseWriter, r *http.Request) {
+		json.NewEncoder(w).Encode(schema.PricingGetResponse{
+			Pricing: schema.Pricing{
+				Currency: "EUR",
+			},
+		})
+	})
+	ctx := context.Background()
+
+	pricing, _, err := env.Client.Pricing.Get(ctx)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if pricing.Image.PerGBMonth.Currency != "EUR" {
+		t.Errorf("unexpected currency: %v", pricing.Image.PerGBMonth.Currency)
+	}
+}
diff --git a/hcloud/schema_test.go b/hcloud/schema_test.go
index 322d751..ee2258d 100644
--- a/hcloud/schema_test.go
+++ b/hcloud/schema_test.go
@@ -493,7 +493,20 @@ func TestServerTypeFromSchema(t *testing.T) {
 		"cores": 4,
 		"memory": 1.0,
 		"disk": 20,
-		"storage_type": "local"
+		"storage_type": "local",
+		"prices": [
+			{
+				"location": "fsn1",
+				"price_hourly": {
+					"net": "1",
+					"gross": "1.19"
+				},
+				"price_monthly": {
+					"net": "1",
+					"gross": "1.19"
+				}
+			}
+		]
 	}`)
 
 	var s schema.ServerType
@@ -523,6 +536,25 @@ func TestServerTypeFromSchema(t *testing.T) {
 	if serverType.StorageType != StorageTypeLocal {
 		t.Errorf("unexpected storage type: %q", serverType.StorageType)
 	}
+	if len(serverType.Pricings) != 1 {
+		t.Errorf("unexpected number of pricings: %d", len(serverType.Pricings))
+	} else {
+		if serverType.Pricings[0].Location.Name != "fsn1" {
+			t.Errorf("unexpected location name: %v", serverType.Pricings[0].Location.Name)
+		}
+		if serverType.Pricings[0].Hourly.Net != "1" {
+			t.Errorf("unexpected hourly net price: %v", serverType.Pricings[0].Hourly.Net)
+		}
+		if serverType.Pricings[0].Hourly.Gross != "1.19" {
+			t.Errorf("unexpected hourly gross price: %v", serverType.Pricings[0].Hourly.Gross)
+		}
+		if serverType.Pricings[0].Monthly.Net != "1" {
+			t.Errorf("unexpected monthly net price: %v", serverType.Pricings[0].Monthly.Net)
+		}
+		if serverType.Pricings[0].Monthly.Gross != "1.19" {
+			t.Errorf("unexpected monthly gross price: %v", serverType.Pricings[0].Monthly.Gross)
+		}
+	}
 }
 
 func TestSSHKeyFromSchema(t *testing.T) {
@@ -722,3 +754,146 @@ func TestImageFromSchema(t *testing.T) {
 		t.Errorf("unexpected RapidDeploy: %v", image.RapidDeploy)
 	}
 }
+
+func TestPricingFromSchema(t *testing.T) {
+	data := []byte(`{
+		"currency": "EUR",
+		"vat_rate": "19.00",
+		"image": {
+			"price_per_gb_month": {
+				"net": "1",
+				"gross": "1.19"
+			}
+		},
+		"floating_ip": {
+			"price_monthly": {
+				"net": "1",
+				"gross": "1.19"
+			}
+		},
+		"traffic": {
+			"price_per_gb": {
+				"net": "1",
+				"gross": "1.19"
+			}
+		},
+		"server_backup": {
+			"percentage": "20"
+		},
+		"server_types": [
+			{
+				"id": 4,
+				"name": "CX11",
+				"prices": [
+					{
+						"location": "fsn1",
+						"price_hourly": {
+							"net": "1",
+							"gross": "1.19"
+						},
+						"price_monthly": {
+							"net": "1",
+							"gross": "1.19"
+						}
+					}
+				]
+			}
+		]
+	}`)
+
+	var s schema.Pricing
+	if err := json.Unmarshal(data, &s); err != nil {
+		t.Fatal(err)
+	}
+	pricing := PricingFromSchema(s)
+
+	if pricing.Image.PerGBMonth.Currency != "EUR" {
+		t.Errorf("unexpected Image.PerGBMonth.Currency: %v", pricing.Image.PerGBMonth.Currency)
+	}
+	if pricing.Image.PerGBMonth.VATRate != "19.00" {
+		t.Errorf("unexpected Image.PerGBMonth.VATRate: %v", pricing.Image.PerGBMonth.VATRate)
+	}
+	if pricing.Image.PerGBMonth.Net != "1" {
+		t.Errorf("unexpected Image.PerGBMonth.Net: %v", pricing.Image.PerGBMonth.Net)
+	}
+	if pricing.Image.PerGBMonth.Gross != "1.19" {
+		t.Errorf("unexpected Image.PerGBMonth.Gross: %v", pricing.Image.PerGBMonth.Gross)
+	}
+
+	if pricing.FloatingIP.Monthly.Currency != "EUR" {
+		t.Errorf("unexpected FloatingIP.Monthly.Currency: %v", pricing.FloatingIP.Monthly.Currency)
+	}
+	if pricing.FloatingIP.Monthly.VATRate != "19.00" {
+		t.Errorf("unexpected FloatingIP.Monthly.VATRate: %v", pricing.FloatingIP.Monthly.VATRate)
+	}
+	if pricing.FloatingIP.Monthly.Net != "1" {
+		t.Errorf("unexpected FloatingIP.Monthly.Net: %v", pricing.FloatingIP.Monthly.Net)
+	}
+	if pricing.FloatingIP.Monthly.Gross != "1.19" {
+		t.Errorf("unexpected FloatingIP.Monthly.Gross: %v", pricing.FloatingIP.Monthly.Gross)
+	}
+
+	if pricing.Traffic.PerGB.Currency != "EUR" {
+		t.Errorf("unexpected Traffic.PerGB.Currency: %v", pricing.Traffic.PerGB.Currency)
+	}
+	if pricing.Traffic.PerGB.VATRate != "19.00" {
+		t.Errorf("unexpected Traffic.PerGB.VATRate: %v", pricing.Traffic.PerGB.VATRate)
+	}
+	if pricing.Traffic.PerGB.Net != "1" {
+		t.Errorf("unexpected Traffic.PerGB.Net: %v", pricing.Traffic.PerGB.Net)
+	}
+	if pricing.Traffic.PerGB.Gross != "1.19" {
+		t.Errorf("unexpected Traffic.PerGB.Gross: %v", pricing.Traffic.PerGB.Gross)
+	}
+
+	if pricing.ServerBackup.Percentage != "20" {
+		t.Errorf("unexpected ServerBackup.Percentage: %v", pricing.ServerBackup.Percentage)
+	}
+
+	if len(pricing.ServerTypes) != 1 {
+		t.Errorf("unexpected number of server types: %d", len(pricing.ServerTypes))
+	} else {
+		p := pricing.ServerTypes[0]
+
+		if p.ServerType.ID != 4 {
+			t.Errorf("unexpected ServerType.ID: %d", p.ServerType.ID)
+		}
+		if p.ServerType.Name != "CX11" {
+			t.Errorf("unexpected ServerType.Name: %v", p.ServerType.Name)
+		}
+
+		if len(p.Pricings) != 1 {
+			t.Errorf("unexpected number of prices: %d", len(p.Pricings))
+		} else {
+			if p.Pricings[0].Location.Name != "fsn1" {
+				t.Errorf("unexpected Location.Name: %v", p.Pricings[0].Location.Name)
+			}
+
+			if p.Pricings[0].Hourly.Currency != "EUR" {
+				t.Errorf("unexpected Hourly.Currency: %v", p.Pricings[0].Hourly.Currency)
+			}
+			if p.Pricings[0].Hourly.VATRate != "19.00" {
+				t.Errorf("unexpected Hourly.VATRate: %v", p.Pricings[0].Hourly.VATRate)
+			}
+			if p.Pricings[0].Hourly.Net != "1" {
+				t.Errorf("unexpected Hourly.Net: %v", p.Pricings[0].Hourly.Net)
+			}
+			if p.Pricings[0].Hourly.Gross != "1.19" {
+				t.Errorf("unexpected Hourly.Gross: %v", p.Pricings[0].Hourly.Gross)
+			}
+
+			if p.Pricings[0].Monthly.Currency != "EUR" {
+				t.Errorf("unexpected Monthly.Currency: %v", p.Pricings[0].Monthly.Currency)
+			}
+			if p.Pricings[0].Monthly.VATRate != "19.00" {
+				t.Errorf("unexpected Monthly.VATRate: %v", p.Pricings[0].Monthly.VATRate)
+			}
+			if p.Pricings[0].Monthly.Net != "1" {
+				t.Errorf("unexpected Monthly.Net: %v", p.Pricings[0].Monthly.Net)
+			}
+			if p.Pricings[0].Monthly.Gross != "1.19" {
+				t.Errorf("unexpected Monthly.Gross: %v", p.Pricings[0].Monthly.Gross)
+			}
+		}
+	}
+}