Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions core/human/v2/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package human

import "fmt"

// UnknownFieldError is returned when a selected field path does not exist in
// the data, so callers like the printer can suggest the valid fields.
type UnknownFieldError struct {
FieldName string
ValidFields []string
}

func (e *UnknownFieldError) Error() string {
return fmt.Sprintf("unknown field %s, valid fields are: %v", e.FieldName, e.ValidFields)
}
93 changes: 93 additions & 0 deletions core/human/v2/format.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package human

import (
"fmt"
"reflect"
"strings"
"time"

"github.com/dustin/go-humanize"
"github.com/scaleway/scaleway-sdk-go/scw"
)

type Formatter interface {
FormatHuman() (string, error)
}

func Format(data any) (string, error) {
// Dereference pointers until the underlying value is reached, letting any
// value in the chain escape through FormatHuman.
value := reflect.ValueOf(data)
for value.Kind() == reflect.Pointer {
if value.IsNil() {
return "-", nil
}
if f, ok := reflect.TypeAssert[Formatter](value); ok {
return f.FormatHuman()
}
value = value.Elem()
}
if !value.IsValid() {
return "-", nil
}
if f, ok := reflect.TypeAssert[Formatter](value); ok {
return f.FormatHuman()
}
data = value.Interface()

switch v := data.(type) {
case string:
if v == "" {
return "-", nil
}

return v, nil
case time.Time:
return humanize.Time(v), nil
case scw.Size:
if isIECNotation := v%1024 == 0 && v%1000 != 0; isIECNotation {
return humanize.IBytes(uint64(v)), nil
}

return humanize.Bytes(uint64(v)), nil
case scw.Duration:
return formatDuration(v), nil
}

return fmt.Sprint(data), nil
}

// formatDuration renders a duration as its joined non-zero components,
// matching v1's registered marshaler for scw.Duration.
func formatDuration(v scw.Duration) string {
const (
minutes = int64(60)
hours = 60 * minutes
days = 24 * hours
)
d := v.Seconds / days
h := (v.Seconds - d*days) / hours
m := (v.Seconds - (d*days + h*hours)) / minutes
s := v.Seconds % 60
res := []string(nil)
if d != 0 {
res = append(res, fmt.Sprintf("%d days", d))
}
if h != 0 {
res = append(res, fmt.Sprintf("%d hours", h))
}
if m != 0 {
res = append(res, fmt.Sprintf("%d minutes", m))
}
if s != 0 {
res = append(res, fmt.Sprintf("%d seconds", s))
}
if v.Nanos != 0 {
res = append(res, fmt.Sprintf("%d nanoseconds", v.Nanos))
}
if len(res) == 0 {
return "0 seconds"
}

return strings.Join(res, " ")
}
173 changes: 173 additions & 0 deletions core/human/v2/format_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package human_test

import (
"testing"
"time"

"github.com/dustin/go-humanize"
human "github.com/scaleway/scaleway-cli/v2/core/human/v2"
"github.com/scaleway/scaleway-sdk-go/scw"
"github.com/stretchr/testify/require"
)

type FormatterStruct struct {
Foo string
}

func (FormatterStruct) FormatHuman() (string, error) {
return "cell-formatted", nil
}

type StringerStruct struct{}

func (StringerStruct) String() string {
return "stringed"
}

type ErrorStruct struct{}

func (ErrorStruct) Error() string {
return "something broke"
}

func TestFormat(t *testing.T) {
date := time.Date(1990, 11, 17, 20, 20, 0, 0, time.UTC)
humanDate := humanize.Time(date)

for _, tc := range []struct {
name string
data any
want string
wantErr string // substring of the expected error, empty to expect success
}{
{
name: "formatter value",
data: FormatterStruct{Foo: "ignored"},
want: "cell-formatted",
},
{
name: "formatter pointer",
data: &FormatterStruct{Foo: "ignored"},
want: "cell-formatted",
},
{
name: "formatter multi-pointer",
data: new(new(&FormatterStruct{Foo: "ignored"})),
want: "cell-formatted",
},
{
name: "string",
data: "hello",
want: "hello",
},
{
name: "integer",
data: 42,
want: "42",
},
{
name: "boolean",
data: true,
want: "true",
},
{
name: "empty string",
data: "",
want: "-",
},
{
name: "nil",
data: nil,
want: "-",
},
{
name: "stringer value",
data: StringerStruct{},
want: "stringed",
},
{
name: "stringer pointer",
data: &StringerStruct{},
want: "stringed",
},
{
name: "error",
data: ErrorStruct{},
want: "something broke",
},
{
name: "pointer to integer",
data: new(int(42)),
want: "42",
},
{
name: "nil pointer",
data: (*int)(nil),
want: "-",
},
{
name: "float",
data: 3.14,
want: "3.14",
},
{
name: "time",
data: date,
want: humanDate,
},
{
name: "nil time pointer",
data: (*time.Time)(nil),
want: "-",
},
{
name: "size",
data: scw.Size(13200),
want: "13 kB",
},
{
name: "size IEC notation",
data: scw.Size(52428800),
want: "50 MiB",
},
{
name: "nil size pointer",
data: (*scw.Size)(nil),
want: "-",
},
{
name: "duration",
data: scw.Duration{Seconds: 604800},
want: "7 days",
},
{
name: "duration combined",
data: scw.Duration{Seconds: 9000},
want: "2 hours 30 minutes",
},
{
name: "duration zero",
data: scw.Duration{},
want: "0 seconds",
},
{
name: "duration pointer",
data: &scw.Duration{Seconds: 30},
want: "30 seconds",
},
} {
t.Run(tc.name, func(t *testing.T) {
got, err := human.Format(tc.data)

if tc.wantErr != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tc.wantErr)

return
}

require.NoError(t, err)
require.Equal(t, tc.want, got)
})
}
}
Loading
Loading