diff --git a/bindings/comments_test.go b/bindings/comments_test.go new file mode 100644 index 0000000..4e6c6b1 --- /dev/null +++ b/bindings/comments_test.go @@ -0,0 +1,72 @@ +package bindings_test + +import ( + "fmt" + "go/token" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/guts/bindings" +) + +func TestSyntheticComments(t *testing.T) { + b, err := bindings.New() + require.NoError(t, err) + + str := bindings.LiteralKeyword(bindings.KeywordString) + param := &bindings.TypeParameter{ + Name: bindings.Identifier{ + Name: "testparam", + Package: nil, + Prefix: "", + }, + Modifiers: nil, + Type: &str, + DefaultType: nil, + //SupportComments: bindings.SupportComments{}, + } + //param.LeadingComment("a type parameter") + + exp := &bindings.Interface{ + Name: bindings.Identifier{ + Name: "TestingInterface", + }, + Modifiers: nil, + Fields: nil, + Parameters: []*bindings.TypeParameter{ + param, + }, + Heritage: nil, + Source: bindings.Source{ + File: "test.go", + Position: token.Position{ + Filename: "test.go", + Offset: 0, + Line: 5, + Column: 10, + }, + }, + } + + exp.AppendComment(bindings.SyntheticComment{ + Leading: true, + SingleLine: true, + Text: "hello world", + TrailingNewLine: false, + }) + + exp.AppendComment(bindings.SyntheticComment{ + Leading: false, + SingleLine: true, + Text: "goodbye world", + TrailingNewLine: false, + }) + + node, err := b.ToTypescriptNode(exp) + require.NoError(t, err) + + ts, err := b.SerializeToTypescript(node) + require.NoError(t, err) + fmt.Println(ts) +} diff --git a/config/mutations.go b/config/mutations.go index d75346e..de77483 100644 --- a/config/mutations.go +++ b/config/mutations.go @@ -158,15 +158,7 @@ func EnumLists(ts *guts.Typescript) { values = append(values, t) } - // Pluralize the name - name := key + "s" - switch key[len(key)-1] { - case 'x', 's', 'z': - name = key + "es" - } - if strings.HasSuffix(key, "ch") || strings.HasSuffix(key, "sh") { - name = key + "es" - } + name := pluralize(key) addNodes[name] = &bindings.VariableStatement{ Modifiers: []bindings.Modifier{}, @@ -384,6 +376,35 @@ func InterfaceToType(ts *guts.Typescript) { }) } +// pluralize applies a best-effort English pluralization rule to a name +// (for example "Policy" -> "Policies", "Box" -> "Boxes", "User" -> "Users"). +// +// The heuristic only handles common regular cases. It does not try to +// recognize already-plural inputs ("Updates" -> "Updateses") or irregular +// plurals ("Person" -> "Persons", not "People"). +func pluralize(key string) string { + if key == "" { + return key + } + if strings.HasSuffix(key, "ch") || strings.HasSuffix(key, "sh") { + return key + "es" + } + switch key[len(key)-1] { + case 'x', 's', 'z': + return key + "es" + case 'y': + // consonant + y -> ies, vowel + y -> just s. + if len(key) >= 2 && !isVowel(key[len(key)-2]) { + return key[:len(key)-1] + "ies" + } + } + return key + "s" +} + +func isVowel(b byte) bool { + return strings.IndexByte("aeiouAEIOU", b) >= 0 +} + func isGoEnum(n bindings.Node) (*bindings.Alias, *bindings.UnionType, bool) { al, ok := n.(*bindings.Alias) if !ok { diff --git a/config/pluralize_internal_test.go b/config/pluralize_internal_test.go new file mode 100644 index 0000000..5904b7a --- /dev/null +++ b/config/pluralize_internal_test.go @@ -0,0 +1,53 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPluralize(t *testing.T) { + t.Parallel() + + cases := []struct { + in string + want string + }{ + // Default: just add "s". + {"User", "Users"}, + {"Audience", "Audiences"}, + + // Ends in x, s, z: add "es". + {"Box", "Boxes"}, + {"Bus", "Buses"}, + {"Buzz", "Buzzes"}, + + // Ends in ch, sh: add "es". + {"Church", "Churches"}, + {"Bush", "Bushes"}, + + // Consonant + y: drop "y", add "ies". + {"Policy", "Policies"}, + {"Category", "Categories"}, + {"Story", "Stories"}, + {"City", "Cities"}, + {"HealthSeverity", "HealthSeverities"}, + + // Vowel + y: just add "s". + {"Day", "Days"}, + {"Key", "Keys"}, + {"Boy", "Boys"}, + + // Single-character edge cases. + {"", ""}, + {"y", "ys"}, + {"A", "As"}, + } + + for _, c := range cases { + t.Run(c.in, func(t *testing.T) { + t.Parallel() + require.Equal(t, c.want, pluralize(c.in)) + }) + } +} diff --git a/go.mod b/go.mod index 3fbab14..01e44ae 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/coder/guts -go 1.24.0 +go 1.24.4 require ( github.com/dop251/goja v0.0.0-20241024094426-79f3a7efcdbd @@ -11,16 +11,18 @@ require ( ) require ( + github.com/Masterminds/semver/v3 v3.3.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dlclark/regexp2 v1.11.4 // indirect + github.com/dlclark/regexp2 v1.11.5 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect golang.org/x/mod v0.31.0 // indirect golang.org/x/sync v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/text v0.32.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 2b51133..393f522 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,18 @@ -github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= -github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= +github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= -github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= +github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dop251/goja v0.0.0-20241024094426-79f3a7efcdbd h1:QMSNEh9uQkDjyPwu/J541GgSH+4hw+0skJDIj9HJ3mE= github.com/dop251/goja v0.0.0-20241024094426-79f3a7efcdbd/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U= github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -26,16 +26,16 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsK github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8= github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= diff --git a/playground/index.html b/playground/index.html new file mode 100644 index 0000000..0e27eab --- /dev/null +++ b/playground/index.html @@ -0,0 +1,152 @@ + + + + + + Golang to TypeScript Converter + + + + +
+

Golang to TypeScript Converter

+
+ +
+ +
+ +
+
+
Go
+
+
+
+
+
TypeScript
+
+
+
+ + + + \ No newline at end of file diff --git a/testdata/enumtypes/enumtypes.go b/testdata/enumtypes/enumtypes.go index 460e13b..ff0bedd 100644 --- a/testdata/enumtypes/enumtypes.go +++ b/testdata/enumtypes/enumtypes.go @@ -35,3 +35,10 @@ const ( EnumAliasBoolean EnumAlias = "bool" EnumAliasListString EnumAlias = "list(string)" ) + +type Policy string + +const ( + EnumPolicyAllow Policy = "allow" + EnumPolicyDeny Policy = "deny" +) diff --git a/testdata/enumtypes/enumtypes.ts b/testdata/enumtypes/enumtypes.ts index 2771035..0b0fdb5 100644 --- a/testdata/enumtypes/enumtypes.ts +++ b/testdata/enumtypes/enumtypes.ts @@ -22,3 +22,8 @@ export type EnumSliceType = readonly EnumString[]; export type EnumString = "bar" | "baz" | "foo" | "qux"; export const EnumStrings: EnumString[] = ["bar", "baz", "foo", "qux"]; + +export const Policies: Policy[] = ["allow", "deny"]; + +// From enumtypes/enumtypes.go +export type Policy = "allow" | "deny";