Skip to content

Commit aafd62a

Browse files
authored
feat: support multiple name convention for component and field names (ignite#1236)
1 parent f86a581 commit aafd62a

File tree

59 files changed

+976
-670
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+976
-670
lines changed

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
- The `--release` flag for the `build` command adds the ability to release binaries in a tarball with a checksum file.
88
- Added the flag `--no-default-module` to the command `starport app` to prevent scaffolding a default module when creating a new app
9+
- Added support for multiple naming conventions for component names and field names
910

1011
## `v0.16.1`
1112

integration/cmd_app_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func TestGenerateAnApp(t *testing.T) {
2626

2727
func TestGenerateAnAppWithNoDefaultModule(t *testing.T) {
2828
var (
29-
env = newEnv(t)
29+
env = newEnv(t)
3030
appName = "blog"
3131
)
3232

@@ -56,7 +56,6 @@ func TestGenerateAnAppWithNoDefaultModule(t *testing.T) {
5656
env.EnsureAppIsSteady(path)
5757
}
5858

59-
6059
func TestGenerateAnAppWithWasm(t *testing.T) {
6160
var (
6261
env = newEnv(t)

integration/cmd_ibc_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func TestCreateIBCPacket(t *testing.T) {
6969

7070
env.Must(env.Exec("create a packet",
7171
step.NewSteps(step.New(
72-
step.Exec("starport", "packet", "bar", "text", "--module", "foo", "--ack", "ack1:string,ack2:int,ack3:bool"),
72+
step.Exec("starport", "packet", "bar", "text", "--module", "foo", "--ack", "foo:string,bar:int,foobar:bool"),
7373
step.Workdir(path),
7474
)),
7575
))

integration/cmd_message_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,22 @@ func TestGenerateAnAppWithMessage(t *testing.T) {
1616

1717
env.Must(env.Exec("create a message",
1818
step.NewSteps(step.New(
19-
step.Exec("starport", "message", "foo", "text", "vote:int", "like:bool", "-r", "foo,bar:int,foobar:bool"),
19+
step.Exec("starport", "message", "do-foo", "text", "vote:int", "like:bool", "-r", "foo,bar:int,foobar:bool"),
2020
step.Workdir(path),
2121
)),
2222
))
2323

2424
env.Must(env.Exec("should prevent creating an existing message",
2525
step.NewSteps(step.New(
26-
step.Exec("starport", "message", "foo", "bar"),
26+
step.Exec("starport", "message", "do-foo", "bar"),
2727
step.Workdir(path),
2828
)),
2929
ExecShouldError(),
3030
))
3131

3232
env.Must(env.Exec("create a second message",
3333
step.NewSteps(step.New(
34-
step.Exec("starport", "message", "bar", "bar"),
34+
step.Exec("starport", "message", "do-bar", "bar"),
3535
step.Workdir(path),
3636
)),
3737
))
@@ -45,7 +45,7 @@ func TestGenerateAnAppWithMessage(t *testing.T) {
4545

4646
env.Must(env.Exec("create a message in a module",
4747
step.NewSteps(step.New(
48-
step.Exec("starport", "message", "foo", "text", "--module", "foo", "--desc", "foo bar foobar", "--response", "foo,bar:int,foobar:bool"),
48+
step.Exec("starport", "message", "do-foo", "text", "--module", "foo", "--desc", "foo bar foobar", "--response", "foo,bar:int,foobar:bool"),
4949
step.Workdir(path),
5050
)),
5151
))

starport/pkg/field/field.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Package field provides methods to parse a field provided in a command with the format name:type
2+
package field
3+
4+
import (
5+
"fmt"
6+
"strings"
7+
8+
"github.com/tendermint/starport/starport/pkg/multiformatname"
9+
)
10+
11+
const (
12+
TypeString = "string"
13+
TypeBool = "bool"
14+
TypeInt32 = "int32"
15+
TypeUint64 = "uint64"
16+
)
17+
18+
// Field represents a field inside a structure for a component
19+
// it can a field contained in a type or inside the response of a query, etc...
20+
type Field struct {
21+
Name multiformatname.Name
22+
Datatype string
23+
DatatypeName string
24+
}
25+
26+
// parseFields parses the provided fields, analyses the types and checks there is no duplicated field
27+
func ParseFields(fields []string, isForbiddenField func(string) error) ([]Field, error) {
28+
// Used to check duplicated field
29+
existingFields := make(map[string]bool)
30+
31+
var parsedFields []Field
32+
for _, field := range fields {
33+
fieldSplit := strings.Split(field, ":")
34+
if len(fieldSplit) > 2 {
35+
return parsedFields, fmt.Errorf("invalid field format: %s, should be 'name' or 'name:type'", field)
36+
}
37+
38+
name, err := multiformatname.NewName(fieldSplit[0])
39+
if err != nil {
40+
return parsedFields, err
41+
}
42+
43+
// Ensure the field name is not a Go reserved name, it would generate an incorrect code
44+
if err := isForbiddenField(name.LowerCamel); err != nil {
45+
return parsedFields, fmt.Errorf("%s can't be used as a field name: %s", name, err.Error())
46+
}
47+
48+
// Ensure the field is not duplicated
49+
if _, exists := existingFields[name.LowerCamel]; exists {
50+
return parsedFields, fmt.Errorf("the field %s is duplicated", name.Original)
51+
}
52+
existingFields[name.LowerCamel] = true
53+
54+
// Parse the type if it is provided, otherwise string is used by defaut
55+
datatypeName, datatype := TypeString, TypeString
56+
isTypeSpecified := len(fieldSplit) == 2
57+
if isTypeSpecified {
58+
acceptedTypes := map[string]string{
59+
"string": TypeString,
60+
"bool": TypeBool,
61+
"int": TypeInt32,
62+
"uint": TypeUint64,
63+
}
64+
65+
if t, ok := acceptedTypes[fieldSplit[1]]; ok {
66+
datatype = t
67+
datatypeName = fieldSplit[1]
68+
} else {
69+
return parsedFields, fmt.Errorf("the field type %s doesn't exist", fieldSplit[1])
70+
}
71+
}
72+
73+
parsedFields = append(parsedFields, Field{
74+
Name: name,
75+
Datatype: datatype,
76+
DatatypeName: datatypeName,
77+
})
78+
}
79+
80+
return parsedFields, nil
81+
}

starport/pkg/field/field_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package field_test
2+
3+
import (
4+
"errors"
5+
"github.com/stretchr/testify/require"
6+
"github.com/tendermint/starport/starport/pkg/field"
7+
"github.com/tendermint/starport/starport/pkg/multiformatname"
8+
"testing"
9+
)
10+
11+
var (
12+
noCheck = func(string) error { return nil }
13+
alwaysInvalid = func(string) error { return errors.New("invalid name") }
14+
)
15+
16+
type testCases struct {
17+
provided []string
18+
expected []field.Field
19+
}
20+
21+
func TestParseFields(t *testing.T) {
22+
names := []string{
23+
"foo",
24+
"bar",
25+
"fooBar",
26+
"bar-foo",
27+
"foo_foo",
28+
}
29+
cases := testCases{
30+
provided: []string{
31+
names[0],
32+
names[1] + ":string",
33+
names[2] + ":bool",
34+
names[3] + ":int",
35+
names[4] + ":uint",
36+
},
37+
expected: []field.Field{
38+
{
39+
Datatype: "string",
40+
DatatypeName: "string",
41+
},
42+
{
43+
Datatype: "string",
44+
DatatypeName: "string",
45+
},
46+
{
47+
Datatype: "bool",
48+
DatatypeName: "bool",
49+
},
50+
{
51+
Datatype: "int32",
52+
DatatypeName: "int",
53+
},
54+
{
55+
Datatype: "uint64",
56+
DatatypeName: "uint",
57+
},
58+
},
59+
}
60+
for i, name := range names {
61+
cases.expected[i].Name, _ = multiformatname.NewName(name)
62+
}
63+
64+
actual, err := field.ParseFields(cases.provided, noCheck)
65+
require.NoError(t, err)
66+
require.Equal(t, cases.expected, actual)
67+
68+
// No field provided
69+
actual, err = field.ParseFields([]string{}, noCheck)
70+
require.NoError(t, err)
71+
require.Empty(t, actual)
72+
}
73+
74+
func TestParseFields2(t *testing.T) {
75+
// test failing cases
76+
77+
// check doesn't pass
78+
_, err := field.ParseFields([]string{"foo"}, alwaysInvalid)
79+
require.Error(t, err)
80+
81+
// duplicated field
82+
_, err = field.ParseFields([]string{"foo", "foo:int"}, noCheck)
83+
require.Error(t, err)
84+
85+
// invalid type
86+
_, err = field.ParseFields([]string{"foo:invalid"}, alwaysInvalid)
87+
require.Error(t, err)
88+
89+
// invalid field name
90+
_, err = field.ParseFields([]string{"foo@bar:int"}, alwaysInvalid)
91+
require.Error(t, err)
92+
93+
// invalid format
94+
_, err = field.ParseFields([]string{"foo:int:int"}, alwaysInvalid)
95+
require.Error(t, err)
96+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Package multiformatname provides names automatically converted into multiple naming convention
2+
package multiformatname
3+
4+
import (
5+
"errors"
6+
"fmt"
7+
8+
"github.com/iancoleman/strcase"
9+
)
10+
11+
// MultiFormatName represents a name with multiple naming convention representations
12+
// Supported naming convention are: camel, pascal, and kebab cases
13+
type Name struct {
14+
Original string
15+
LowerCamel string
16+
UpperCamel string
17+
Kebab string
18+
}
19+
20+
// NewMultiFormatName returns a new multi-format name from a name
21+
func NewName(name string) (Name, error) {
22+
if err := CheckName(name); err != nil {
23+
return Name{}, err
24+
}
25+
26+
return Name{
27+
Original: name,
28+
LowerCamel: strcase.ToLowerCamel(name),
29+
UpperCamel: strcase.ToCamel(name),
30+
Kebab: strcase.ToKebab(name),
31+
}, nil
32+
}
33+
34+
// CheckName checks name validity
35+
func CheckName(name string) error {
36+
if name == "" {
37+
return errors.New("name cannot be empty")
38+
}
39+
40+
// check characters
41+
c := name[0]
42+
authorized := ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z')
43+
if !authorized {
44+
return fmt.Errorf("name cannot contain %v as first character", string(c))
45+
}
46+
47+
for _, c := range name[1:] {
48+
// A name can contains letter, hyphen or underscore
49+
authorized := ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9') || c == '-' || c == '_'
50+
if !authorized {
51+
return fmt.Errorf("name cannot contain %v", string(c))
52+
}
53+
}
54+
55+
return nil
56+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package multiformatname_test
2+
3+
import (
4+
"fmt"
5+
"github.com/stretchr/testify/require"
6+
"github.com/tendermint/starport/starport/pkg/multiformatname"
7+
"testing"
8+
)
9+
10+
func TestNewMultiFormatName(t *testing.T) {
11+
// [valueToTest, lowerCamel, upperCamel, kebabCase]
12+
cases := [][4]string{
13+
{"foo", "foo", "Foo", "foo"},
14+
{"fooBar", "fooBar", "FooBar", "foo-bar"},
15+
{"foo-bar", "fooBar", "FooBar", "foo-bar"},
16+
{"foo_bar", "fooBar", "FooBar", "foo-bar"},
17+
{"foo_barFoobar", "fooBarFoobar", "FooBarFoobar", "foo-bar-foobar"},
18+
{"foo_-_bar", "fooBar", "FooBar", "foo---bar"},
19+
{"foo_-_Bar", "fooBar", "FooBar", "foo---bar"},
20+
{"fooBAR", "fooBAR", "FooBAR", "foo-bar"},
21+
{"fooBar123", "fooBar123", "FooBar123", "foo-bar-123"},
22+
}
23+
24+
// test cases
25+
for _, testCase := range cases {
26+
name, err := multiformatname.NewName(testCase[0])
27+
require.NoError(t, err)
28+
require.Equal(
29+
t,
30+
testCase[0],
31+
name.Original,
32+
)
33+
require.Equal(
34+
t,
35+
testCase[1],
36+
name.LowerCamel,
37+
fmt.Sprintf("%s should be converted to the correct lower camel format", testCase[0]),
38+
)
39+
require.Equal(
40+
t,
41+
testCase[2],
42+
name.UpperCamel,
43+
fmt.Sprintf("%s should be converted the correct upper camel format", testCase[0]),
44+
)
45+
require.Equal(
46+
t,
47+
testCase[3],
48+
name.Kebab,
49+
fmt.Sprintf("%s should be converted the correct kebab format", testCase[0]),
50+
)
51+
}
52+
}
53+
54+
func TestNewMultiFormatName2(t *testing.T) {
55+
// Test forbidden names
56+
cases := []string{
57+
"",
58+
"foo bar",
59+
"1foo",
60+
"-foo",
61+
"_foo",
62+
"@foo",
63+
}
64+
65+
// test cases
66+
for _, testCase := range cases {
67+
_, err := multiformatname.NewName(testCase)
68+
require.Error(t, err)
69+
}
70+
}

0 commit comments

Comments
 (0)