Skip to content

Commit

Permalink
refactor: use new price formatter (#817)
Browse files Browse the repository at this point in the history
Centralize the formatting of prices shown in `hcloud server-type
describe ...` and `hcloud load-balancer-type describe ...`.

---------

Co-authored-by: Jonas L. <[email protected]>
  • Loading branch information
apricote and jooola authored Jul 17, 2024
1 parent 7f94cdf commit a75c787
Show file tree
Hide file tree
Showing 11 changed files with 281 additions and 12 deletions.
36 changes: 30 additions & 6 deletions internal/cmd/loadbalancertype/describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/spf13/cobra"

"github.com/hetznercloud/cli/internal/cmd/base"
"github.com/hetznercloud/cli/internal/cmd/util"
"github.com/hetznercloud/cli/internal/hcapi2"
"github.com/hetznercloud/cli/internal/state"
"github.com/hetznercloud/hcloud-go/v2/hcloud"
Expand All @@ -22,7 +23,7 @@ var DescribeCmd = base.DescribeCmd{
}
return lbt, hcloud.SchemaFromLoadBalancerType(lbt), nil
},
PrintText: func(_ state.State, cmd *cobra.Command, resource interface{}) error {
PrintText: func(s state.State, cmd *cobra.Command, resource interface{}) error {
loadBalancerType := resource.(*hcloud.LoadBalancerType)

cmd.Printf("ID:\t\t\t\t%d\n", loadBalancerType.ID)
Expand All @@ -33,12 +34,35 @@ var DescribeCmd = base.DescribeCmd{
cmd.Printf("Max Targets:\t\t\t%d\n", loadBalancerType.MaxTargets)
cmd.Printf("Max assigned Certificates:\t%d\n", loadBalancerType.MaxAssignedCertificates)

cmd.Printf("Pricings per Location:\n")
for _, price := range loadBalancerType.Pricings {
cmd.Printf(" - Location:\t%s:\n", price.Location.Name)
cmd.Printf(" Hourly:\t€ %s\n", price.Hourly.Gross)
cmd.Printf(" Monthly:\t€ %s\n", price.Monthly.Gross)
pricings, err := fullPricingInfo(s, loadBalancerType)
if err != nil {
cmd.PrintErrf("failed to get prices for load balancer type: %v", err)
}

if pricings != nil {
cmd.Printf("Pricings per Location:\n")
for _, price := range pricings {
cmd.Printf(" - Location:\t%s\n", price.Location.Name)
cmd.Printf(" Hourly:\t%s\n", util.GrossPrice(price.Hourly))
cmd.Printf(" Monthly:\t%s\n", util.GrossPrice(price.Monthly))
}
}

return nil
},
}

func fullPricingInfo(s state.State, loadBalancerType *hcloud.LoadBalancerType) ([]hcloud.LoadBalancerTypeLocationPricing, error) {
pricing, _, err := s.Client().Pricing().Get(s)
if err != nil {
return nil, err
}

for _, price := range pricing.LoadBalancerTypes {
if price.LoadBalancerType.ID == loadBalancerType.ID {
return price.Pricings, nil
}
}

return nil, nil
}
43 changes: 43 additions & 0 deletions internal/cmd/loadbalancertype/describe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,46 @@ func TestDescribe(t *testing.T) {
MaxAssignedCertificates: 10,
}, nil, nil)

fx.Client.PricingClient.EXPECT().
Get(gomock.Any()).
Return(hcloud.Pricing{
LoadBalancerTypes: []hcloud.LoadBalancerTypePricing{
// Two load balancer types to test that fullPricingInfo filters for the correct one
{
LoadBalancerType: &hcloud.LoadBalancerType{ID: 1},
Pricings: []hcloud.LoadBalancerTypeLocationPricing{{
Location: &hcloud.Location{
Name: "Nuremberg",
},
Hourly: hcloud.Price{
Gross: "4.0000",
Currency: "EUR",
},
Monthly: hcloud.Price{
Gross: "7.0000",
Currency: "EUR",
},
}},
},
{
LoadBalancerType: &hcloud.LoadBalancerType{ID: 123},
Pricings: []hcloud.LoadBalancerTypeLocationPricing{{
Location: &hcloud.Location{
Name: "Falkenstein",
},
Hourly: hcloud.Price{
Gross: "1.0000",
Currency: "EUR",
},
Monthly: hcloud.Price{
Gross: "2.0000",
Currency: "EUR",
},
}},
},
},
}, nil, nil)

out, errOut, err := fx.Run(cmd, []string{"lb11"})

expOut := `ID: 123
Expand All @@ -40,6 +80,9 @@ Max Connections: 10000
Max Targets: 25
Max assigned Certificates: 10
Pricings per Location:
- Location: Falkenstein
Hourly: € 1.0000
Monthly: € 2.0000
`

assert.NoError(t, err)
Expand Down
35 changes: 29 additions & 6 deletions internal/cmd/servertype/describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ var DescribeCmd = base.DescribeCmd{
}
return st, hcloud.SchemaFromServerType(st), nil
},
PrintText: func(_ state.State, cmd *cobra.Command, resource interface{}) error {
PrintText: func(s state.State, cmd *cobra.Command, resource interface{}) error {
serverType := resource.(*hcloud.ServerType)

cmd.Printf("ID:\t\t\t%d\n", serverType.ID)
Expand All @@ -38,12 +38,35 @@ var DescribeCmd = base.DescribeCmd{
cmd.Printf("Included Traffic:\t%d TB\n", serverType.IncludedTraffic/util.Tebibyte)
cmd.Printf(util.DescribeDeprecation(serverType))

cmd.Printf("Pricings per Location:\n")
for _, price := range serverType.Pricings {
cmd.Printf(" - Location:\t%s:\n", price.Location.Name)
cmd.Printf(" Hourly:\t€ %s\n", price.Hourly.Gross)
cmd.Printf(" Monthly:\t€ %s\n", price.Monthly.Gross)
pricings, err := fullPricingInfo(s, serverType)
if err != nil {
cmd.PrintErrf("failed to get prices for server type: %v", err)
}

if pricings != nil {
cmd.Printf("Pricings per Location:\n")
for _, price := range pricings {
cmd.Printf(" - Location:\t%s\n", price.Location.Name)
cmd.Printf(" Hourly:\t%s\n", util.GrossPrice(price.Hourly))
cmd.Printf(" Monthly:\t%s\n", util.GrossPrice(price.Monthly))
}
}

return nil
},
}

func fullPricingInfo(s state.State, serverType *hcloud.ServerType) ([]hcloud.ServerTypeLocationPricing, error) {
pricing, _, err := s.Client().Pricing().Get(s)
if err != nil {
return nil, err
}

for _, price := range pricing.ServerTypes {
if price.ServerType.ID == serverType.ID {
return price.Pricings, nil
}
}

return nil, nil
}
43 changes: 43 additions & 0 deletions internal/cmd/servertype/describe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,46 @@ func TestDescribe(t *testing.T) {
StorageType: hcloud.StorageTypeLocal,
}, nil, nil)

fx.Client.PricingClient.EXPECT().
Get(gomock.Any()).
Return(hcloud.Pricing{
ServerTypes: []hcloud.ServerTypePricing{
// Two server types to test that fullPricingInfo filters for the correct one
{
ServerType: &hcloud.ServerType{ID: 1},
Pricings: []hcloud.ServerTypeLocationPricing{{
Location: &hcloud.Location{
Name: "Nuremberg",
},
Hourly: hcloud.Price{
Gross: "4.0000",
Currency: "EUR",
},
Monthly: hcloud.Price{
Gross: "7.0000",
Currency: "EUR",
},
}},
},
{
ServerType: &hcloud.ServerType{ID: 45},
Pricings: []hcloud.ServerTypeLocationPricing{{
Location: &hcloud.Location{
Name: "Falkenstein",
},
Hourly: hcloud.Price{
Gross: "1.0000",
Currency: "EUR",
},
Monthly: hcloud.Price{
Gross: "2.0000",
Currency: "EUR",
},
}},
},
},
}, nil, nil)

out, errOut, err := fx.Run(cmd, []string{"cax11"})

expOut := `ID: 45
Expand All @@ -44,6 +84,9 @@ Disk: 40 GB
Storage Type: local
Included Traffic: 0 TB
Pricings per Location:
- Location: Falkenstein
Hourly: € 1.0000
Monthly: € 2.0000
`

assert.NoError(t, err)
Expand Down
16 changes: 16 additions & 0 deletions internal/cmd/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import (
"github.com/spf13/cast"
"github.com/spf13/cobra"
"github.com/spf13/pflag"

"github.com/hetznercloud/hcloud-go/v2/hcloud"
)

func YesNo(b bool) string {
Expand Down Expand Up @@ -58,6 +60,20 @@ func Age(t, currentTime time.Time) string {
return "just now"
}

func GrossPrice(price hcloud.Price) string {
currencyDisplay := price.Currency

// Currency is the ISO 4217 code, but we want to the show currency symbol
switch price.Currency {
case "EUR":
currencyDisplay = "€"
}

// The code/symbol and the amount are separated by a non-breaking space:
// https://style-guide.europa.eu/en/content/-/isg/topic?identifier=7.3.3-rules-for-expressing-monetary-units#id370303__id370303_PositionISO
return fmt.Sprintf("%s\u00a0%s", currencyDisplay, price.Gross)
}

func ChainRunE(fns ...func(cmd *cobra.Command, args []string) error) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
for _, fn := range fns {
Expand Down
29 changes: 29 additions & 0 deletions internal/cmd/util/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/hetznercloud/cli/internal/cmd/util"
"github.com/hetznercloud/cli/internal/testutil"
"github.com/hetznercloud/hcloud-go/v2/hcloud"
)

func TestYesNo(t *testing.T) {
Expand Down Expand Up @@ -422,3 +423,31 @@ func TestRemoveDuplicates(t *testing.T) {
assert.Equal(t, []string{"a"}, util.RemoveDuplicates([]string{"a", "a", "a", "a", "a"}))
assert.Equal(t, []int{1, 2, 3, 4, 5}, util.RemoveDuplicates([]int{1, 2, 1, 1, 3, 2, 1, 4, 3, 2, 5, 4, 3, 2, 1}))
}

func TestPrice(t *testing.T) {
tests := []struct {
name string
price hcloud.Price
amount string
currency string
want string
}{
{
name: "known currency",
price: hcloud.Price{Currency: "EUR", Gross: "5.00"},
want: "€\u00a05.00",
},
{
name: "unknown currency",
price: hcloud.Price{Currency: "HOL", Gross: "1.2"},
amount: "1.2",
currency: "HOL",
want: "HOL\u00a01.2",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, util.GrossPrice(tt.price), "GrossPrice(%v, %v)", tt.amount, tt.currency)
})
}
}
11 changes: 11 additions & 0 deletions internal/hcapi2/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Client interface {
PlacementGroup() PlacementGroupClient
RDNS() RDNSClient
PrimaryIP() PrimaryIPClient
Pricing() PricingClient
WithOpts(...hcloud.ClientOption)
}

Expand All @@ -48,6 +49,7 @@ type clientCache struct {
placementGroupClient PlacementGroupClient
rdnsClient RDNSClient
primaryIPClient PrimaryIPClient
pricingClient PricingClient
}

type client struct {
Expand Down Expand Up @@ -237,3 +239,12 @@ func (c *client) PlacementGroup() PlacementGroupClient {
defer c.mu.Unlock()
return c.cache.placementGroupClient
}

func (c *client) Pricing() PricingClient {
c.mu.Lock()
if c.cache.pricingClient == nil {
c.cache.pricingClient = NewPricingClient(&c.client.Pricing)
}
defer c.mu.Unlock()
return c.cache.pricingClient
}
6 changes: 6 additions & 0 deletions internal/hcapi2/mock/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type MockClient struct {
ISOClient *MockISOClient
PlacementGroupClient *MockPlacementGroupClient
RDNSClient *MockRDNSClient
PricingClient *MockPricingClient
}

func NewMockClient(ctrl *gomock.Controller) *MockClient {
Expand All @@ -49,6 +50,7 @@ func NewMockClient(ctrl *gomock.Controller) *MockClient {
ISOClient: NewMockISOClient(ctrl),
PlacementGroupClient: NewMockPlacementGroupClient(ctrl),
RDNSClient: NewMockRDNSClient(ctrl),
PricingClient: NewMockPricingClient(ctrl),
}
}

Expand Down Expand Up @@ -123,6 +125,10 @@ func (c *MockClient) PlacementGroup() hcapi2.PlacementGroupClient {
return c.PlacementGroupClient
}

func (c *MockClient) Pricing() hcapi2.PricingClient {
return c.PricingClient
}

func (*MockClient) WithOpts(_ ...hcloud.ClientOption) {
// no-op
}
Expand Down
1 change: 1 addition & 0 deletions internal/hcapi2/mock/mock_gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ package hcapi2_mock
//go:generate go run github.com/golang/mock/mockgen -package hcapi2_mock -destination zz_volume_client_mock.go github.com/hetznercloud/cli/internal/hcapi2 VolumeClient
//go:generate go run github.com/golang/mock/mockgen -package hcapi2_mock -destination zz_placement_group_client_mock.go github.com/hetznercloud/cli/internal/hcapi2 PlacementGroupClient
//go:generate go run github.com/golang/mock/mockgen -package hcapi2_mock -destination zz_rdns_client_mock.go github.com/hetznercloud/cli/internal/hcapi2 RDNSClient
//go:generate go run github.com/golang/mock/mockgen -package hcapi2_mock -destination zz_pricing_client_mock.go github.com/hetznercloud/cli/internal/hcapi2 PricingClient
Loading

0 comments on commit a75c787

Please sign in to comment.