Skip to content

Commit 2728b08

Browse files
committed
feat: Add camelCase fieldNames and json:",omitempty" support
- Add ability to convert to JSII compatible camelCased fields - Support valid `json:",omitempty"` tag (with test cases) - Add `-local-pkg` flag for running tscriptify from local directory References: - tkrajina#73 - tkrajina#70 - https://github.com/rogpeppe/gohack
1 parent e807dc8 commit 2728b08

File tree

8 files changed

+682
-281
lines changed

8 files changed

+682
-281
lines changed

README.md

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,22 @@ Command line options:
5050

5151
```
5252
$ tscriptify --help
53-
Usage of tscriptify:
5453
-all-optional
55-
Create interfaces with all fields optional
54+
Set all fields optional
5655
-backup string
5756
Directory where backup files are saved
57+
-camel-case
58+
Convert all field names to camelCase
5859
-import value
5960
Typescript import for your custom type, repeat this option for each import needed
6061
-interface
6162
Create interfaces (not classes)
63+
-local-pkg
64+
Replace github.com/GoodNotes/typescriptify-golang-structs with the current directory in go.mod file. Useful for local development.
6265
-package string
6366
Path of the package with models
6467
-readonly
65-
Create interfaces with readonly fields
68+
Set all fields readonly
6669
-target string
6770
Target typescript file
6871
-verbose
@@ -71,7 +74,7 @@ Usage of tscriptify:
7174

7275
## Models and conversion
7376

74-
If the `Person` structs contain a reference to the `Address` struct, then you don't have to add `Address` explicitly. Only fields with a valid `json` tag will be converted to TypeScript models.
77+
If the `Person` structs contain a reference to the `Address` struct, then you don't have to add `Address` explicitly. Any public field will be converted to TypeScript models.
7578

7679
Example input structs:
7780

@@ -217,7 +220,7 @@ class Address {
217220

218221
The lines between `//[Address:]` and `//[end]` will be left intact after `ConvertToFile()`.
219222

220-
If your custom code contain methods, then just casting yout object to the target class (with `<Person> {...}`) won't work because the casted object won't contain your methods.
223+
If your custom code contain methods, then just casting your object to the target class (with `<Person> {...}`) won't work because the casted object won't contain your methods.
221224

222225
In that case use the constructor:
223226

@@ -459,6 +462,40 @@ Below snippet shows how to set the field `ObjectType` of the above `SecretDescri
459462
AddTypeWithName(sdTypeTagged, "SecretDescriptor")
460463
```
461464

465+
Conversion of field names to camelCase can be achieved using the `WithCamelCase` method:
466+
467+
```golang
468+
type PersonalInfo struct {
469+
Hobbies []string `json:",omitempty"`
470+
PetName string `json:",omitempty"`
471+
}
472+
type CloudKitDev struct {
473+
Name string
474+
PersonalInfo PersonalInfo
475+
}
476+
477+
t := typescriptify.New()
478+
t.CreateInterface = true
479+
t.ReadOnlyFields = true
480+
t.CamelCaseFields = true
481+
t.BackupDir = ""
482+
483+
t.AddType(reflect.TypeOf(CloudKitDev{}))
484+
```
485+
486+
The resulting code will be:
487+
488+
```typescript
489+
export interface PersonalInfo {
490+
readonly hobbies?: string[];
491+
readonly petName?: string;
492+
}
493+
export interface CloudKitDev {
494+
readonly name: string;
495+
readonly personalInfo: PersonalInfo;
496+
}
497+
```
498+
462499
> Note: In both of these cases use the `AddTypeWithName` method to explicitly provide the name for the generated TypeScript interface.
463500
> By design `reflect.Type` returns an empty string for non-defined types.
464501
>
Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,33 @@
1-
package models
2-
3-
type Address struct {
4-
// Used in html
5-
City string `json:"city"`
6-
Number float64 `json:"number"`
7-
Country string `json:"country,omitempty"`
8-
}
9-
10-
type PersonalInfo struct {
11-
Hobbies []string `json:"hobby"`
12-
PetName string `json:"pet_name"`
13-
}
14-
15-
type Person struct {
16-
Name string `json:"name"`
17-
PersonalInfo PersonalInfo `json:"personal_info"`
18-
Nicknames []string `json:"nicknames"`
19-
Addresses []Address `json:"addresses"`
20-
Address *Address `json:"address"`
21-
Metadata []byte `json:"metadata" ts_type:"{[key:string]:string}"`
22-
Friends []*Person `json:"friends"`
23-
}
1+
package models
2+
3+
type Address struct {
4+
// Used in html
5+
City string `json:"city"`
6+
Number float64 `json:"number"`
7+
Country string `json:"country,omitempty"`
8+
}
9+
10+
type PersonalInfo struct {
11+
Hobbies []string `json:"hobby"`
12+
PetName string `json:"pet_name"`
13+
}
14+
15+
type Person struct {
16+
Name string `json:"name"`
17+
PersonalInfo PersonalInfo `json:"personal_info"`
18+
Nicknames []string `json:"nicknames"`
19+
Addresses []Address `json:"addresses"`
20+
Address *Address `json:"address"`
21+
Metadata []byte `json:"metadata" ts_type:"{[key:string]:string}"`
22+
Friends []*Person `json:"friends"`
23+
}
24+
25+
type CloudKitDev struct {
26+
Name string
27+
PersonalInfo PersonalInfo
28+
Nicknames []string
29+
Addresses []Address
30+
Address *Address
31+
Metadata []byte
32+
Friends []*Person
33+
}

makefile

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ test: node_modules lint
1212
go run example/example.go
1313
npx tsc browser_test/example_output.ts
1414
# Make sure dommandline tool works:
15-
go run tscriptify/main.go -package github.com/GoodNotes/typescriptify-golang-structs/example/example-models -verbose -target tmp_classes.ts example/example-models/example_models.go
16-
go run tscriptify/main.go -package github.com/GoodNotes/typescriptify-golang-structs/example/example-models -verbose -target tmp_interfaces.ts -interface example/example-models/example_models.go
17-
go run tscriptify/main.go -package=github.com/aws/secrets-store-csi-driver-provider-aws/provider -verbose -target=tmp_jsiiIntefaces.ts -interface -readonly -all-optional SecretDescriptor
15+
go run tscriptify/main.go -package github.com/GoodNotes/typescriptify-golang-structs/example/example-models -local-pkg -verbose -target tmp_classes.ts example/example-models/example_models.go
16+
go run tscriptify/main.go -package github.com/GoodNotes/typescriptify-golang-structs/example/example-models -local-pkg -verbose -target tmp_interfaces.ts -interface example/example-models/example_models.go
17+
go run tscriptify/main.go -package github.com/GoodNotes/typescriptify-golang-structs/example/example-models -local-pkg -verbose -target tmp_jsiiInterfaces.ts -readonly -all-optional -camel-case -interface CloudKitDev
18+
go run tscriptify/main.go -package=github.com/aws/secrets-store-csi-driver-provider-aws/provider -local-pkg -verbose -target=tmp_jsiiSecretDescriptor.ts -interface -readonly -all-optional SecretDescriptor
1819

1920
.PHONY: lint
2021
lint:

tscriptify/main.go

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ func main() {
4444
t := typescriptify.New()
4545
t.CreateInterface = {{ .Interface }}
4646
t.ReadOnlyFields = {{ .Readonly }}
47+
t.CamelCaseFields = {{ .CamelCase }}
4748
{{ range $key, $value := .InitParams }} t.{{ $key }}={{ $value }}
4849
{{ end }}
4950
{{ if .AllOptional }}
@@ -71,6 +72,8 @@ type Params struct {
7172
Interface bool
7273
Readonly bool
7374
AllOptional bool
75+
CamelCase bool
76+
LocalPkg bool
7477
Verbose bool
7578
}
7679

@@ -81,9 +84,11 @@ func main() {
8184
flag.StringVar(&p.TargetFile, "target", "", "Target typescript file")
8285
flag.StringVar(&backupDir, "backup", "", "Directory where backup files are saved")
8386
flag.BoolVar(&p.Interface, "interface", false, "Create interfaces (not classes)")
84-
flag.BoolVar(&p.Readonly, "readonly", false, "Create interfaces with readonly fields")
85-
flag.BoolVar(&p.AllOptional, "all-optional", false, "Create interfaces with all fields optional")
87+
flag.BoolVar(&p.Readonly, "readonly", false, "Set all fields readonly")
88+
flag.BoolVar(&p.AllOptional, "all-optional", false, "Set all fields optional")
89+
flag.BoolVar(&p.CamelCase, "camel-case", false, "Convert all field names to camelCase")
8690
flag.Var(&p.CustomImports, "import", "Typescript import for your custom type, repeat this option for each import needed")
91+
flag.BoolVar(&p.LocalPkg, "local-pkg", false, "Replace github.com/GoodNotes/typescriptify-golang-structs with the current directory in go.mod file. Useful for local development.")
8792
flag.BoolVar(&p.Verbose, "verbose", false, "Verbose logs")
8893
flag.Parse()
8994

@@ -140,39 +145,47 @@ func main() {
140145
handleErr(err)
141146
fmt.Printf("\nCompiling generated code (%s):\n%s\n----------------------------------------------------------------------------------------------------\n", f.Name(), string(byts))
142147
}
148+
executeCommand(d, nil, "go", "mod", "init", "tmp")
143149

144-
var cmd *exec.Cmd
145-
cmdInit := exec.Command("go", "mod", "init", "tmp")
146-
fmt.Println(d + ": " + strings.Join(cmdInit.Args, " "))
147-
cmdInit.Dir = d
148-
initOutput, err := cmdInit.CombinedOutput()
149-
if err != nil {
150-
fmt.Println(string(initOutput))
150+
if p.LocalPkg {
151+
// replace github.com/GoodNotes/typescriptify-golang-structs with the current directory
152+
pwd, err := os.Getwd()
151153
handleErr(err)
154+
executeCommand(d, nil, "go", "mod", "edit", "-replace", "github.com/GoodNotes/typescriptify-golang-structs="+pwd)
152155
}
153-
fmt.Println(string(initOutput))
154-
cmdGet := exec.Command("go", "get", "-v")
155-
cmdGet.Env = append(os.Environ(), "GO111MODULE=on")
156-
fmt.Println(d + ": " + strings.Join(cmdGet.Args, " "))
157-
cmdGet.Dir = d
158-
getOutput, err := cmdGet.CombinedOutput()
159-
if err != nil {
160-
fmt.Println(string(getOutput))
161-
handleErr(err)
156+
157+
cmdGet := []string{"go", "get", "-v"}
158+
environ := append(os.Environ(), "GO111MODULE=on")
159+
executeCommand(d, environ, cmdGet...)
160+
161+
executeCommand(d, nil, "go", "run", ".")
162+
163+
err = os.Rename(filepath.Join(d, p.TargetFile), p.TargetFile)
164+
handleErr(err)
165+
}
166+
167+
// cmdDir: Directory to execute command from
168+
// env: Environment variables (Optional, Pass nil if not required)
169+
// args: Command arguments
170+
func executeCommand(cmdDir string, env []string, args ...string) string {
171+
cmd := exec.Command(args[0], args[1:]...)
172+
173+
// Assign environment variables, if provided.
174+
if env != nil {
175+
cmd.Env = env
162176
}
163-
fmt.Println(string(getOutput))
164-
cmd = exec.Command("go", "run", ".")
165-
cmd.Dir = d
166-
fmt.Println(d + ": " + strings.Join(cmd.Args, " "))
177+
178+
fmt.Println(cmdDir + ": " + strings.Join(cmd.Args, " "))
179+
cmd.Dir = cmdDir
167180

168181
output, err := cmd.CombinedOutput()
169182
if err != nil {
170183
fmt.Println(string(output))
171184
handleErr(err)
172185
}
173186
fmt.Println(string(output))
174-
err = os.Rename(filepath.Join(d, p.TargetFile), p.TargetFile)
175-
handleErr(err)
187+
188+
return string(output)
176189
}
177190

178191
func GetGolangFileStructs(filename string) ([]string, error) {

typescriptify/typescriptify.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ type TypeScriptify struct {
149149
DontExport bool
150150
CreateInterface bool
151151
ReadOnlyFields bool
152+
CamelCaseFields bool
153+
CamelCaseOptions *CamelCaseOptions
152154
customImports []string
153155

154156
structTypes []StructType
@@ -262,6 +264,12 @@ func (t *TypeScriptify) WithReadonlyFields(b bool) *TypeScriptify {
262264
return t
263265
}
264266

267+
func (t *TypeScriptify) WithCamelCaseFields(b bool, opts *CamelCaseOptions) *TypeScriptify {
268+
t.CamelCaseFields = b
269+
t.CamelCaseOptions = opts
270+
return t
271+
}
272+
265273
func (t *TypeScriptify) WithConstructor(b bool) *TypeScriptify {
266274
t.CreateConstructor = b
267275
return t
@@ -596,12 +604,16 @@ func (t *TypeScriptify) getJSONFieldName(field reflect.StructField, isPtr bool)
596604
jsonTagParts := strings.Split(jsonTag, ",")
597605
if len(jsonTagParts) > 0 {
598606
jsonFieldName = strings.Trim(jsonTagParts[0], t.Indent)
607+
//`json:",omitempty"` is valid
608+
if jsonFieldName == "" {
609+
jsonFieldName = field.Name
610+
}
599611
}
600612
hasOmitEmpty := false
601613
ignored := false
602614
for _, t := range jsonTagParts {
603615
if t == "" {
604-
break
616+
continue
605617
}
606618
if t == "omitempty" {
607619
hasOmitEmpty = true
@@ -618,6 +630,9 @@ func (t *TypeScriptify) getJSONFieldName(field reflect.StructField, isPtr bool)
618630
} else if /*field.IsExported()*/ field.PkgPath == "" {
619631
jsonFieldName = field.Name
620632
}
633+
if t.CamelCaseFields {
634+
jsonFieldName = CamelCase(jsonFieldName, t.CamelCaseOptions)
635+
}
621636
return jsonFieldName
622637
}
623638

0 commit comments

Comments
 (0)