Skip to content

Commit

Permalink
feat: Add camelCase fieldNames and json:",omitempty" support
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
vincentgna committed Feb 4, 2024
1 parent e807dc8 commit 2728b08
Show file tree
Hide file tree
Showing 8 changed files with 682 additions and 281 deletions.
47 changes: 42 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,22 @@ Command line options:

```
$ tscriptify --help
Usage of tscriptify:
-all-optional
Create interfaces with all fields optional
Set all fields optional
-backup string
Directory where backup files are saved
-camel-case
Convert all field names to camelCase
-import value
Typescript import for your custom type, repeat this option for each import needed
-interface
Create interfaces (not classes)
-local-pkg
Replace github.com/GoodNotes/typescriptify-golang-structs with the current directory in go.mod file. Useful for local development.
-package string
Path of the package with models
-readonly
Create interfaces with readonly fields
Set all fields readonly
-target string
Target typescript file
-verbose
Expand All @@ -71,7 +74,7 @@ Usage of tscriptify:

## Models and conversion

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.
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.

Example input structs:

Expand Down Expand Up @@ -217,7 +220,7 @@ class Address {

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

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.
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.

In that case use the constructor:

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

Conversion of field names to camelCase can be achieved using the `WithCamelCase` method:

```golang
type PersonalInfo struct {
Hobbies []string `json:",omitempty"`
PetName string `json:",omitempty"`
}
type CloudKitDev struct {
Name string
PersonalInfo PersonalInfo
}

t := typescriptify.New()
t.CreateInterface = true
t.ReadOnlyFields = true
t.CamelCaseFields = true
t.BackupDir = ""

t.AddType(reflect.TypeOf(CloudKitDev{}))
```

The resulting code will be:

```typescript
export interface PersonalInfo {
readonly hobbies?: string[];
readonly petName?: string;
}
export interface CloudKitDev {
readonly name: string;
readonly personalInfo: PersonalInfo;
}
```

> Note: In both of these cases use the `AddTypeWithName` method to explicitly provide the name for the generated TypeScript interface.
> By design `reflect.Type` returns an empty string for non-defined types.
>
Expand Down
56 changes: 33 additions & 23 deletions example/example-models/example_models.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
package models

type Address struct {
// Used in html
City string `json:"city"`
Number float64 `json:"number"`
Country string `json:"country,omitempty"`
}

type PersonalInfo struct {
Hobbies []string `json:"hobby"`
PetName string `json:"pet_name"`
}

type Person struct {
Name string `json:"name"`
PersonalInfo PersonalInfo `json:"personal_info"`
Nicknames []string `json:"nicknames"`
Addresses []Address `json:"addresses"`
Address *Address `json:"address"`
Metadata []byte `json:"metadata" ts_type:"{[key:string]:string}"`
Friends []*Person `json:"friends"`
}
package models

type Address struct {
// Used in html
City string `json:"city"`
Number float64 `json:"number"`
Country string `json:"country,omitempty"`
}

type PersonalInfo struct {
Hobbies []string `json:"hobby"`
PetName string `json:"pet_name"`
}

type Person struct {
Name string `json:"name"`
PersonalInfo PersonalInfo `json:"personal_info"`
Nicknames []string `json:"nicknames"`
Addresses []Address `json:"addresses"`
Address *Address `json:"address"`
Metadata []byte `json:"metadata" ts_type:"{[key:string]:string}"`
Friends []*Person `json:"friends"`
}

type CloudKitDev struct {
Name string
PersonalInfo PersonalInfo
Nicknames []string
Addresses []Address
Address *Address
Metadata []byte
Friends []*Person
}
7 changes: 4 additions & 3 deletions makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ test: node_modules lint
go run example/example.go
npx tsc browser_test/example_output.ts
# Make sure dommandline tool works:
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
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
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
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
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
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
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

.PHONY: lint
lint:
Expand Down
61 changes: 37 additions & 24 deletions tscriptify/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func main() {
t := typescriptify.New()
t.CreateInterface = {{ .Interface }}
t.ReadOnlyFields = {{ .Readonly }}
t.CamelCaseFields = {{ .CamelCase }}
{{ range $key, $value := .InitParams }} t.{{ $key }}={{ $value }}
{{ end }}
{{ if .AllOptional }}
Expand Down Expand Up @@ -71,6 +72,8 @@ type Params struct {
Interface bool
Readonly bool
AllOptional bool
CamelCase bool
LocalPkg bool
Verbose bool
}

Expand All @@ -81,9 +84,11 @@ func main() {
flag.StringVar(&p.TargetFile, "target", "", "Target typescript file")
flag.StringVar(&backupDir, "backup", "", "Directory where backup files are saved")
flag.BoolVar(&p.Interface, "interface", false, "Create interfaces (not classes)")
flag.BoolVar(&p.Readonly, "readonly", false, "Create interfaces with readonly fields")
flag.BoolVar(&p.AllOptional, "all-optional", false, "Create interfaces with all fields optional")
flag.BoolVar(&p.Readonly, "readonly", false, "Set all fields readonly")
flag.BoolVar(&p.AllOptional, "all-optional", false, "Set all fields optional")
flag.BoolVar(&p.CamelCase, "camel-case", false, "Convert all field names to camelCase")
flag.Var(&p.CustomImports, "import", "Typescript import for your custom type, repeat this option for each import needed")
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.")
flag.BoolVar(&p.Verbose, "verbose", false, "Verbose logs")
flag.Parse()

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

var cmd *exec.Cmd
cmdInit := exec.Command("go", "mod", "init", "tmp")
fmt.Println(d + ": " + strings.Join(cmdInit.Args, " "))
cmdInit.Dir = d
initOutput, err := cmdInit.CombinedOutput()
if err != nil {
fmt.Println(string(initOutput))
if p.LocalPkg {
// replace github.com/GoodNotes/typescriptify-golang-structs with the current directory
pwd, err := os.Getwd()
handleErr(err)
executeCommand(d, nil, "go", "mod", "edit", "-replace", "github.com/GoodNotes/typescriptify-golang-structs="+pwd)
}
fmt.Println(string(initOutput))
cmdGet := exec.Command("go", "get", "-v")
cmdGet.Env = append(os.Environ(), "GO111MODULE=on")
fmt.Println(d + ": " + strings.Join(cmdGet.Args, " "))
cmdGet.Dir = d
getOutput, err := cmdGet.CombinedOutput()
if err != nil {
fmt.Println(string(getOutput))
handleErr(err)

cmdGet := []string{"go", "get", "-v"}
environ := append(os.Environ(), "GO111MODULE=on")
executeCommand(d, environ, cmdGet...)

executeCommand(d, nil, "go", "run", ".")

err = os.Rename(filepath.Join(d, p.TargetFile), p.TargetFile)
handleErr(err)
}

// cmdDir: Directory to execute command from
// env: Environment variables (Optional, Pass nil if not required)
// args: Command arguments
func executeCommand(cmdDir string, env []string, args ...string) string {
cmd := exec.Command(args[0], args[1:]...)

// Assign environment variables, if provided.
if env != nil {
cmd.Env = env
}
fmt.Println(string(getOutput))
cmd = exec.Command("go", "run", ".")
cmd.Dir = d
fmt.Println(d + ": " + strings.Join(cmd.Args, " "))

fmt.Println(cmdDir + ": " + strings.Join(cmd.Args, " "))
cmd.Dir = cmdDir

output, err := cmd.CombinedOutput()
if err != nil {
fmt.Println(string(output))
handleErr(err)
}
fmt.Println(string(output))
err = os.Rename(filepath.Join(d, p.TargetFile), p.TargetFile)
handleErr(err)

return string(output)
}

func GetGolangFileStructs(filename string) ([]string, error) {
Expand Down
17 changes: 16 additions & 1 deletion typescriptify/typescriptify.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ type TypeScriptify struct {
DontExport bool
CreateInterface bool
ReadOnlyFields bool
CamelCaseFields bool
CamelCaseOptions *CamelCaseOptions
customImports []string

structTypes []StructType
Expand Down Expand Up @@ -262,6 +264,12 @@ func (t *TypeScriptify) WithReadonlyFields(b bool) *TypeScriptify {
return t
}

func (t *TypeScriptify) WithCamelCaseFields(b bool, opts *CamelCaseOptions) *TypeScriptify {
t.CamelCaseFields = b
t.CamelCaseOptions = opts
return t
}

func (t *TypeScriptify) WithConstructor(b bool) *TypeScriptify {
t.CreateConstructor = b
return t
Expand Down Expand Up @@ -596,12 +604,16 @@ func (t *TypeScriptify) getJSONFieldName(field reflect.StructField, isPtr bool)
jsonTagParts := strings.Split(jsonTag, ",")
if len(jsonTagParts) > 0 {
jsonFieldName = strings.Trim(jsonTagParts[0], t.Indent)
//`json:",omitempty"` is valid
if jsonFieldName == "" {
jsonFieldName = field.Name
}
}
hasOmitEmpty := false
ignored := false
for _, t := range jsonTagParts {
if t == "" {
break
continue
}
if t == "omitempty" {
hasOmitEmpty = true
Expand All @@ -618,6 +630,9 @@ func (t *TypeScriptify) getJSONFieldName(field reflect.StructField, isPtr bool)
} else if /*field.IsExported()*/ field.PkgPath == "" {
jsonFieldName = field.Name
}
if t.CamelCaseFields {
jsonFieldName = CamelCase(jsonFieldName, t.CamelCaseOptions)
}
return jsonFieldName
}

Expand Down
Loading

0 comments on commit 2728b08

Please sign in to comment.