Skip to content

Commit 62c05c8

Browse files
rdb: add human marshaller for instance (#1182)
Co-authored-by: Jerome Quere <[email protected]>
1 parent c4a216d commit 62c05c8

12 files changed

+1300
-93
lines changed

internal/human/marshal.go

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,8 @@ func marshalStruct(value reflect.Value, opt *MarshalOpt) (string, error) {
169169
// If type is a struct
170170
// We loop through all struct field
171171
data := [][]string(nil)
172-
for i := 0; i < value.NumField(); i++ {
173-
subData, err := marshal(value.Field(i), append(keys, value.Type().Field(i).Name))
172+
for _, fieldIndex := range getStructFieldsIndex(value) {
173+
subData, err := marshal(value.FieldByIndex(fieldIndex), append(keys, value.Type().FieldByIndex(fieldIndex).Name))
174174
if err != nil {
175175
return nil, err
176176
}
@@ -207,6 +207,46 @@ func marshalStruct(value reflect.Value, opt *MarshalOpt) (string, error) {
207207
return strings.TrimSpace(buffer.String()), nil
208208
}
209209

210+
// getStructFieldsIndex will return a list of fieldIndex ([]int) sorted by their position in the Go struct.
211+
// This function will handle anonymous field and make sure that if a field is overwritten only the highest is returned.
212+
// You can use reflect GetFieldByIndex([]int) to get the correct field.
213+
func getStructFieldsIndex(v reflect.Value) [][]int {
214+
// Using a map we make sure only the field with the highest order is returned for a given Name
215+
found := map[string][]int{}
216+
217+
var recFunc func(v reflect.Value, parent []int)
218+
recFunc = func(v reflect.Value, parent []int) {
219+
for i := 0; i < v.NumField(); i++ {
220+
field := v.Type().Field(i)
221+
// If a field is anonymous we start recursive call
222+
if field.Anonymous {
223+
recFunc(v.Field(i), append(parent, i))
224+
} else {
225+
// else we add the field in the found map
226+
found[field.Name] = append(parent, i)
227+
}
228+
}
229+
}
230+
recFunc(v, []int(nil))
231+
232+
result := [][]int(nil)
233+
for _, value := range found {
234+
result = append(result, value)
235+
}
236+
237+
sort.Slice(result, func(i, j int) bool {
238+
n := 0
239+
for n < len(result[i]) && n < len(result[j]) {
240+
if result[i][n] != result[j][n] {
241+
return result[i][n] < result[j][n]
242+
}
243+
}
244+
panic("this can never happen")
245+
})
246+
247+
return result
248+
}
249+
210250
func marshalSlice(slice reflect.Value, opt *MarshalOpt) (string, error) {
211251
// Resole itemType and get rid of all pointer level if needed.
212252
itemType := slice.Type().Elem()

internal/human/marshal_func.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,38 @@ func init() {
6363
v := i.(version.Version)
6464
return v.String(), nil
6565
})
66+
marshalerFuncs.Store(reflect.TypeOf(scw.Duration{}), func(i interface{}, opt *MarshalOpt) (string, error) {
67+
v := i.(scw.Duration)
68+
const (
69+
minutes = int64(60)
70+
hours = 60 * minutes
71+
days = 24 * hours
72+
)
73+
d := v.Seconds / days
74+
h := (v.Seconds - d*days) / hours
75+
m := (v.Seconds - (d*days + h*hours)) / minutes
76+
s := v.Seconds % 60
77+
res := []string(nil)
78+
if d != 0 {
79+
res = append(res, fmt.Sprintf("%d days", d))
80+
}
81+
if h != 0 {
82+
res = append(res, fmt.Sprintf("%d hours", h))
83+
}
84+
if m != 0 {
85+
res = append(res, fmt.Sprintf("%d minutes", m))
86+
}
87+
if s != 0 {
88+
res = append(res, fmt.Sprintf("%d seconds", s))
89+
}
90+
if v.Nanos != 0 {
91+
res = append(res, fmt.Sprintf("%d nanoseconds", v.Nanos))
92+
}
93+
if len(res) == 0 {
94+
return "0 seconds", nil
95+
}
96+
return strings.Join(res, " "), nil
97+
})
6698
}
6799

68100
// TODO: implement the same logic as args.RegisterMarshalFunc(), where i must be a pointer

internal/human/marshal_test.go

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ type Human struct {
4040
Acquaintances []*Acquaintance
4141
}
4242

43+
type NestedAnonymous struct {
44+
Name string
45+
}
46+
47+
type Anonymous struct {
48+
NestedAnonymous
49+
Name string
50+
}
51+
4352
type Stringer struct{}
4453

4554
func (s Stringer) String() string {
@@ -51,7 +60,6 @@ func TestMarshal(t *testing.T) {
5160
data interface{}
5261
opt *MarshalOpt
5362
result string
54-
err error
5563
}
5664

5765
run := func(tc *testCase) func(*testing.T) {
@@ -95,7 +103,6 @@ func TestMarshal(t *testing.T) {
95103
Stringer: Stringer{},
96104
StringerPtr: &Stringer{},
97105
},
98-
opt: nil,
99106
result: `
100107
String This is a string
101108
Int 42
@@ -118,7 +125,6 @@ func TestMarshal(t *testing.T) {
118125
Stringer a stringer
119126
StringerPtr a stringer
120127
`,
121-
err: nil,
122128
}))
123129

124130
t.Run("struct2", run(&testCase{
@@ -152,18 +158,25 @@ func TestMarshal(t *testing.T) {
152158
Dr watson Assistant
153159
Mrs. Hudson Landlady
154160
`,
155-
err: nil,
156161
}))
157162

158163
t.Run("empty string", run(&testCase{
159164
data: "",
160165
result: `-`,
161-
err: nil,
162166
}))
163167

164168
t.Run("nil", run(&testCase{
165169
data: nil,
166170
result: `-`,
167-
err: nil,
171+
}))
172+
173+
t.Run("anonymous", run(&testCase{
174+
data: &Anonymous{
175+
NestedAnonymous: NestedAnonymous{
176+
Name: "John",
177+
},
178+
Name: "Paul",
179+
},
180+
result: `Name Paul`,
168181
}))
169182
}

internal/namespaces/rdb/v1/custom.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package rdb
22

3-
import "github.com/scaleway/scaleway-cli/internal/core"
3+
import (
4+
"github.com/scaleway/scaleway-cli/internal/core"
5+
"github.com/scaleway/scaleway-cli/internal/human"
6+
"github.com/scaleway/scaleway-sdk-go/api/rdb/v1"
7+
)
48

59
var nodeTypes = []string{
610
"DB-DEV-S",
@@ -17,6 +21,9 @@ var nodeTypes = []string{
1721
func GetCommands() *core.Commands {
1822
cmds := GetGeneratedCommands()
1923

24+
human.RegisterMarshalerFunc(rdb.Instance{}, instanceMarshalerFunc)
25+
human.RegisterMarshalerFunc(rdb.BackupSchedule{}, backupScheduleMarshalerFunc)
26+
2027
cmds.Merge(core.NewCommands(
2128
instanceWaitCommand(),
2229
))

internal/namespaces/rdb/v1/custom_instance.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"time"
77

88
"github.com/scaleway/scaleway-cli/internal/core"
9+
"github.com/scaleway/scaleway-cli/internal/human"
910
"github.com/scaleway/scaleway-sdk-go/api/rdb/v1"
1011
"github.com/scaleway/scaleway-sdk-go/scw"
1112
)
@@ -19,6 +20,62 @@ type serverWaitRequest struct {
1920
Region scw.Region
2021
}
2122

23+
func instanceMarshalerFunc(i interface{}, opt *human.MarshalOpt) (string, error) {
24+
// To avoid recursion of human.Marshal we create a dummy type
25+
type tmp rdb.Instance
26+
instance := tmp(i.(rdb.Instance))
27+
28+
// Sections
29+
opt.Sections = []*human.MarshalSection{
30+
{
31+
FieldName: "Endpoint",
32+
},
33+
{
34+
FieldName: "Volume",
35+
},
36+
{
37+
FieldName: "BackupSchedule",
38+
},
39+
{
40+
FieldName: "Settings",
41+
},
42+
}
43+
44+
str, err := human.Marshal(instance, opt)
45+
if err != nil {
46+
return "", err
47+
}
48+
49+
return str, nil
50+
}
51+
52+
func backupScheduleMarshalerFunc(i interface{}, opt *human.MarshalOpt) (string, error) {
53+
// To avoid recursion of human.Marshal we create a dummy type
54+
type LocalBackupSchedule rdb.BackupSchedule
55+
type tmp struct {
56+
LocalBackupSchedule
57+
Frequency *scw.Duration `json:"frequency"`
58+
Retention *scw.Duration `json:"retention"`
59+
}
60+
61+
backupSchedule := tmp{
62+
LocalBackupSchedule: LocalBackupSchedule(i.(rdb.BackupSchedule)),
63+
Frequency: &scw.Duration{
64+
Seconds: int64(i.(rdb.BackupSchedule).Frequency) * 24 * 3600,
65+
},
66+
Retention: &scw.Duration{
67+
Seconds: int64(i.(rdb.BackupSchedule).Retention) * 24 * 3600,
68+
},
69+
}
70+
71+
str, err := human.Marshal(backupSchedule, opt)
72+
if err != nil {
73+
return "", err
74+
}
75+
76+
return str, nil
77+
}
78+
2279
func instanceWaitCommand() *core.Command {
2380
return &core.Command{
2481
Short: `Wait for an instance to reach a stable state`,

internal/namespaces/rdb/v1/custom_instance_create.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package rdb
22

33
import (
44
"context"
5+
"strings"
56

67
"github.com/scaleway/scaleway-cli/internal/core"
78
"github.com/scaleway/scaleway-sdk-go/api/rdb/v1"
@@ -22,5 +23,12 @@ func instanceCreateBuilder(c *core.Command) *core.Command {
2223
})
2324
}
2425

26+
// Waiting for API to accept uppercase node-type
27+
c.Interceptor = func(ctx context.Context, argsI interface{}, runner core.CommandRunner) (interface{}, error) {
28+
args := argsI.(*rdb.CreateInstanceRequest)
29+
args.NodeType = strings.ToLower(args.NodeType)
30+
return runner(ctx, args)
31+
}
32+
2533
return c
2634
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package rdb
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/scaleway/scaleway-cli/internal/core"
8+
)
9+
10+
func Test_GetInstance(t *testing.T) {
11+
t.Run("Simple", core.Test(&core.TestConfig{
12+
Commands: GetCommands(),
13+
BeforeFunc: core.ExecStoreBeforeCmd(
14+
"StartServer",
15+
fmt.Sprintf("scw rdb instance create node-type=DB-DEV-S is-ha-cluster=false name=%s engine=%s user-name=%s password=%s --wait", name, engine, user, password),
16+
),
17+
Cmd: "scw rdb instance get {{ .StartServer.ID }}",
18+
Check: core.TestCheckGolden(),
19+
AfterFunc: core.ExecAfterCmd("scw rdb instance delete {{ .StartServer.ID }}"),
20+
}))
21+
}

internal/namespaces/rdb/v1/testdata/test-clone-instance-simple.golden

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,36 @@
11
🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲
22
🟩🟩🟩 STDOUT️ 🟩🟩🟩️
3-
CreatedAt few seconds ago
4-
Volume.Type lssd
5-
Volume.Size 25 GB
6-
Region fr-par
7-
ID 2fe91a35-e819-4691-bae5-33613fc841dc
8-
Name foobar
9-
OrganizationID 951df375-e094-4d26-97c1-ba548eeb9c42
10-
Status ready
11-
Engine PostgreSQL-12
12-
Endpoint.IP 51.159.26.195
13-
Endpoint.Port 24332
14-
Settings.0.Name work_mem
15-
Settings.0.Value 4
16-
Settings.1.Name max_connections
17-
Settings.1.Value 100
18-
Settings.2.Name effective_cache_size
19-
Settings.2.Value 1300
20-
Settings.3.Name maintenance_work_mem
21-
Settings.3.Value 150
22-
Settings.4.Name max_parallel_workers
23-
Settings.4.Value 0
24-
Settings.5.Name max_parallel_workers_per_gather
25-
Settings.5.Value 0
26-
BackupSchedule.Frequency 24
27-
BackupSchedule.Retention 7
28-
BackupSchedule.Disabled false
29-
IsHaCluster false
30-
NodeType db-dev-m
3+
CreatedAt few seconds ago
4+
Region fr-par
5+
ID 2fe91a35-e819-4691-bae5-33613fc841dc
6+
Name foobar
7+
OrganizationID 951df375-e094-4d26-97c1-ba548eeb9c42
8+
Status ready
9+
Engine PostgreSQL-12
10+
IsHaCluster false
11+
NodeType db-dev-m
12+
13+
Endpoint:
14+
IP 51.159.26.195
15+
Port 24332
16+
17+
Volume:
18+
Type lssd
19+
Size 25 GB
20+
21+
Backup Schedule:
22+
Disabled false
23+
Frequency 24 days
24+
Retention 7 days
25+
26+
Settings:
27+
NAME VALUE
28+
work_mem 4
29+
max_connections 100
30+
effective_cache_size 1300
31+
maintenance_work_mem 150
32+
max_parallel_workers 0
33+
max_parallel_workers_per_gather 0
3134
🟩🟩🟩 JSON STDOUT 🟩🟩🟩
3235
{
3336
"created_at": "1970-01-01T00:00:00.0Z",

0 commit comments

Comments
 (0)