diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b717f86 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +dist/ diff --git a/LICENSE b/LICENSE index 9da83ed..c91721d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 11Wizards +Copyright (c) 2023 11Wizards (U.K.) Limited. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md new file mode 100644 index 0000000..23a49fd --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# go-to-dart + +Go-to-dart helps you convert Go structs to Dart classes that can be used with [json_serializable](https://pub.dev/packages/json_serializable). + +## Features + +- Supports only structs in the same package (no generics or embedded structs yet) +- Supports primitives, slices, maps, and pointers +- Support some other arbitrary types such as `time.Time` and `mo.Option` (easy to extend!) + +Need something more? Please open an issue or even better, a PR! + +## Installation + +```bash +go install github.com/11wizards/go-to-dart +``` + +The above command will install go-to-dart in your `$GOPATH/bin` directory. Make sure that directory is in your `$PATH`. + +## Usage + +```bash +go-to-dart -i ./examples/user -o ./examples/user +``` + +## Example + +Running the command above would take the package `./examples/user` below and generate a file `./examples/user/user.dart`. + +```go +package user + +import ( + "time" +) + +type User struct { + ID int + Name string + Email string + Password string + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt *time.Time + Options map[string]string + Tags []string +} +``` + +Contents of `./examples/user/user.dart`: +```dart +import 'package:json_annotation/json_annotation.dart'; + +part 'user.g.dart'; + +@JsonSerializable() +class User { + @JsonKey(name: 'ID') final int id; + @JsonKey(name: 'Name') final String name; + @JsonKey(name: 'Email') final String email; + @JsonKey(name: 'Password') final String password; + @JsonKey(name: 'CreatedAt') final DateTime createdAt; + @JsonKey(name: 'UpdatedAt') final DateTime updatedAt; + @JsonKey(name: 'DeletedAt') final DateTime? deletedAt; + @JsonKey(name: 'Options') final Map options; + @JsonKey(name: 'Tags') final List tags; + + User({ + required this.id, + required this.name, + required this.email, + required this.password, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.options, + required this.tags, + }); + + Map toJson() => _$UserToJson(this); + + factory User.fromJson(Map json) => _$UserFromJson(json); +} + + +``` \ No newline at end of file diff --git a/examples/everything/everything.dart b/examples/everything/everything.dart new file mode 100644 index 0000000..517f4a2 --- /dev/null +++ b/examples/everything/everything.dart @@ -0,0 +1,92 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'everything.g.dart'; + +@JsonSerializable() +class Parent { + final String id; + final int number1; + final List number2; + final int? number3; + final List? number4; + final List number5; + final int? number6; + final List number7; + final String text1; + final List text2; + final String? text3; + final List? text4; + final List text5; + final String? text6; + final List text7; + final DateTime date1; + final List date2; + final DateTime? date3; + final List? date4; + final List date5; + final String? date6; + final List date7; + final Child child1; + final List child2; + final Child? child3; + final List? child4; + final List child5; + final Child? child6; + final List child7; + final Map map1; + @JsonKey(name: 'map2_weird_name') final Map map2; + + Parent({ + required this.id, + required this.number1, + required this.number2, + this.number3, + this.number4, + required this.number5, + this.number6, + required this.number7, + required this.text1, + required this.text2, + this.text3, + this.text4, + required this.text5, + this.text6, + required this.text7, + required this.date1, + required this.date2, + this.date3, + this.date4, + required this.date5, + this.date6, + required this.date7, + required this.child1, + required this.child2, + this.child3, + this.child4, + required this.child5, + this.child6, + required this.child7, + required this.map1, + required this.map2, + }); + + Map toJson() => _$ParentToJson(this); + + factory Parent.fromJson(Map json) => _$ParentFromJson(json); +} + +@JsonSerializable() +class Child { + final int id; + final String name; + + Child({ + required this.id, + required this.name, + }); + + Map toJson() => _$ChildToJson(this); + + factory Child.fromJson(Map json) => _$ChildFromJson(json); +} + diff --git a/examples/everything/everything.go b/examples/everything/everything.go new file mode 100644 index 0000000..fdf931f --- /dev/null +++ b/examples/everything/everything.go @@ -0,0 +1,54 @@ +package everything + +import ( + "github.com/samber/mo" + "time" +) + +type ParentID string + +type Parent struct { + ID ParentID `json:"id"` + + Number1 int `json:"number1"` + Number2 []int `json:"number2"` + Number3 *int `json:"number3"` + Number4 *[]int `json:"number4"` + Number5 []*int `json:"number5"` + Number6 mo.Option[int] `json:"number6"` + Number7 []mo.Option[int] `json:"number7"` + + Text1 string `json:"text1"` + Text2 []string `json:"text2"` + Text3 *string `json:"text3"` + Text4 *[]string `json:"text4"` + Text5 []*string `json:"text5"` + Text6 mo.Option[string] `json:"text6"` + Text7 []mo.Option[string] `json:"text7"` + + Date1 time.Time `json:"date1"` + Date2 []time.Time `json:"date2"` + Date3 *time.Time `json:"date3"` + Date4 *[]time.Time `json:"date4"` + Date5 []*time.Time `json:"date5"` + Date6 mo.Option[string] `json:"date6"` + Date7 []mo.Option[string] `json:"date7"` + + Child1 Child `json:"child1"` + Child2 []Child `json:"child2"` + Child3 *Child `json:"child3"` + Child4 *[]Child `json:"child4"` + Child5 []*Child `json:"child5"` + Child6 mo.Option[Child] `json:"child6"` + Child7 []mo.Option[Child] `json:"child7"` + + Map1 map[string]float64 `json:"map1"` + Map2 map[ChildID]Child `json:"map2_weird_name"` +} + +type ChildID int64 + +type Child struct { + ID ChildID `json:"id"` + Name string `json:"name"` +} diff --git a/examples/user/user.dart b/examples/user/user.dart new file mode 100644 index 0000000..6cf1dc6 --- /dev/null +++ b/examples/user/user.dart @@ -0,0 +1,33 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'user.g.dart'; + +@JsonSerializable() +class User { + @JsonKey(name: 'ID') final int id; + @JsonKey(name: 'Name') final String name; + @JsonKey(name: 'Email') final String email; + @JsonKey(name: 'Password') final String password; + @JsonKey(name: 'CreatedAt') final DateTime createdAt; + @JsonKey(name: 'UpdatedAt') final DateTime updatedAt; + @JsonKey(name: 'DeletedAt') final DateTime? deletedAt; + @JsonKey(name: 'Options') final Map options; + @JsonKey(name: 'Tags') final List tags; + + User({ + required this.id, + required this.name, + required this.email, + required this.password, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.options, + required this.tags, + }); + + Map toJson() => _$UserToJson(this); + + factory User.fromJson(Map json) => _$UserFromJson(json); +} + diff --git a/examples/user/user.go b/examples/user/user.go new file mode 100644 index 0000000..dd4c41d --- /dev/null +++ b/examples/user/user.go @@ -0,0 +1,17 @@ +package user + +import ( + "time" +) + +type User struct { + ID int + Name string + Email string + Password string + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt *time.Time + Options map[string]string + Tags []string +} diff --git a/generator/class.go b/generator/class.go new file mode 100644 index 0000000..32a88d0 --- /dev/null +++ b/generator/class.go @@ -0,0 +1,54 @@ +package generator + +import ( + "fmt" + "github.com/openconfig/goyang/pkg/indent" + "go/ast" + "io" +) + +func generateFields(wr io.Writer, st *ast.StructType) { + for _, f := range st.Fields.List { + generateFieldDeclaration(wr, f) + fmt.Fprintln(wr, ";") + } + fmt.Fprintln(wr) +} + +func generateConstructor(wr io.Writer, ts *ast.TypeSpec, st *ast.StructType) { + fmt.Fprintf(wr, "%s({\n", ts.Name) + + for _, f := range st.Fields.List { + generateFieldConstrutor(indent.NewWriter(wr, "\t"), f) + fmt.Fprintln(wr, ",") + } + + fmt.Fprintf(wr, "});") + fmt.Fprintln(wr) + fmt.Fprintln(wr) +} + +func generateSerialization(wr io.Writer, ts *ast.TypeSpec) { + fmt.Fprintf(wr, "Map toJson() => _$%sToJson(this);\n\n", ts.Name) +} + +func generateDeserialization(wr io.Writer, ts *ast.TypeSpec) { + fmt.Fprintf(wr, "factory %s.fromJson(Map json) => _$%sFromJson(json);\n", ts.Name, ts.Name) +} + +func generateDartClass(outputFile io.Writer, ts *ast.TypeSpec, st *ast.StructType) bool { + fmt.Fprintln(outputFile, "@JsonSerializable()") + fmt.Fprintf(outputFile, "class %s {\n", ts.Name) + + wr := indent.NewWriter(outputFile, "\t") + + generateFields(wr, st) + generateConstructor(wr, ts, st) + generateSerialization(wr, ts) + generateDeserialization(wr, ts) + + fmt.Fprintln(outputFile, "}") + fmt.Fprintln(outputFile, "") + + return false +} diff --git a/generator/field.go b/generator/field.go new file mode 100644 index 0000000..a3c4efd --- /dev/null +++ b/generator/field.go @@ -0,0 +1,27 @@ +package generator + +import ( + "fmt" + "github.com/11wizards/go-to-dart/generator/format" + "go/ast" + "io" +) + +func generateFieldDeclaration(writer io.Writer, f *ast.Field) { + formatter := format.GetTypeFormatter(f.Type) + fieldName := format.GetFieldName(f) + jsonFieldName := format.GetJSONFieldName(f) + + if jsonFieldName != "" && jsonFieldName != fieldName { + fmt.Fprintf(writer, "@JsonKey(name: '%s') ", jsonFieldName) + } else if jsonFieldName == "" { + fmt.Fprintf(writer, "@JsonKey(name: '%s') ", f.Names[0].Name) + } + + fmt.Fprintf(writer, "final %s", formatter.Declaration(format.GetFieldName(f), f.Type)) +} + +func generateFieldConstrutor(writer io.Writer, f *ast.Field) { + formatter := format.GetTypeFormatter(f.Type) + fmt.Fprint(writer, formatter.Constructor(format.GetFieldName(f), f.Type)) +} diff --git a/generator/format/alias.go b/generator/format/alias.go new file mode 100644 index 0000000..78ee481 --- /dev/null +++ b/generator/format/alias.go @@ -0,0 +1,38 @@ +package format + +import ( + "fmt" + "go/ast" +) + +type AliasFormatter struct { +} + +func (f *AliasFormatter) under(expr ast.Expr) *ast.Ident { + if x, ok := expr.(*ast.Ident); ok && x.Obj != nil { + if y, ok := x.Obj.Decl.(*ast.TypeSpec); ok { + if z, ok := y.Type.(*ast.Ident); ok { + return z + } + } + } + return nil +} + +func (f *AliasFormatter) CanFormat(expr ast.Expr) bool { + return f.under(expr) != nil +} + +func (f *AliasFormatter) Signature(expr ast.Expr) string { + u := f.under(expr) + return GetTypeFormatter(u).Signature(u) +} + +func (f *AliasFormatter) Declaration(fieldName string, expr ast.Expr) string { + return fmt.Sprintf("%s %s", f.Signature(expr), fieldName) +} + +func (f *AliasFormatter) Constructor(fieldName string, expr ast.Expr) string { + u := f.under(expr) + return GetTypeFormatter(u).Constructor(fieldName, u) +} diff --git a/generator/format/array.go b/generator/format/array.go new file mode 100644 index 0000000..8ac9cf7 --- /dev/null +++ b/generator/format/array.go @@ -0,0 +1,33 @@ +package format + +import ( + "fmt" + "go/ast" +) + +type ArrayFormatter struct { +} + +func (f *ArrayFormatter) under(expr ast.Expr) (TypeFormatter, ast.Expr) { + arrayExpr := expr.(*ast.ArrayType) + formatter := GetTypeFormatter(arrayExpr.Elt) + return formatter, arrayExpr.Elt +} + +func (f *ArrayFormatter) CanFormat(expr ast.Expr) bool { + _, ok := expr.(*ast.ArrayType) + return ok +} + +func (f *ArrayFormatter) Signature(expr ast.Expr) string { + formatter, expr := f.under(expr) + return fmt.Sprintf("List<%s>", formatter.Signature(expr)) +} + +func (f *ArrayFormatter) Declaration(fieldName string, expr ast.Expr) string { + return fmt.Sprintf("%s %s", f.Signature(expr), fieldName) +} + +func (f *ArrayFormatter) Constructor(fieldName string, _ ast.Expr) string { + return "required this." + fieldName +} diff --git a/generator/format/format.go b/generator/format/format.go new file mode 100644 index 0000000..340c325 --- /dev/null +++ b/generator/format/format.go @@ -0,0 +1,24 @@ +package format + +import ( + "go/ast" +) + +type TypeFormatter interface { + CanFormat(expr ast.Expr) bool + Signature(expr ast.Expr) string + Declaration(fieldName string, expr ast.Expr) string + Constructor(fieldName string, expr ast.Expr) string +} + +var Formatters []TypeFormatter + +func GetTypeFormatter(expr ast.Expr) TypeFormatter { + for _, f := range Formatters { + if f.CanFormat(expr) { + return f + } + } + + panic("no formatter found for type") +} diff --git a/generator/format/helpers.go b/generator/format/helpers.go new file mode 100644 index 0000000..b86f531 --- /dev/null +++ b/generator/format/helpers.go @@ -0,0 +1,30 @@ +package format + +import ( + "fmt" + "github.com/iancoleman/strcase" + "go/ast" + "reflect" + "strings" +) + +func GetFieldName(f *ast.Field) string { + if f.Names == nil { + panic(fmt.Sprintf("no name for field: %#v", f)) + } + + return strcase.ToLowerCamel(f.Names[0].Name) +} + +func GetJSONFieldName(f *ast.Field) string { + // Check for json struct field tag + if f.Tag != nil { + val := reflect.StructTag(strings.Trim(f.Tag.Value, "`")) + tag, ok := val.Lookup("json") + if ok { + return tag + } + } + + return "" +} diff --git a/generator/format/map.go b/generator/format/map.go new file mode 100644 index 0000000..1923c88 --- /dev/null +++ b/generator/format/map.go @@ -0,0 +1,34 @@ +package format + +import ( + "fmt" + "go/ast" +) + +type MapFormatter struct { +} + +func (f *MapFormatter) under(expr ast.Expr) (TypeFormatter, TypeFormatter, ast.Expr, ast.Expr) { + mapExpr := expr.(*ast.MapType) + keyFormatter := GetTypeFormatter(mapExpr.Key) + valueFormatter := GetTypeFormatter(mapExpr.Value) + return keyFormatter, valueFormatter, mapExpr.Key, mapExpr.Value +} + +func (f *MapFormatter) CanFormat(expr ast.Expr) bool { + _, ok := expr.(*ast.MapType) + return ok +} + +func (f *MapFormatter) Signature(expr ast.Expr) string { + keyFormatter, valueFormatter, keyExpr, valueExpr := f.under(expr) + return fmt.Sprintf("Map<%s, %s>", keyFormatter.Signature(keyExpr), valueFormatter.Signature(valueExpr)) +} + +func (f *MapFormatter) Declaration(fieldName string, expr ast.Expr) string { + return fmt.Sprintf("%s %s", f.Signature(expr), fieldName) +} + +func (f *MapFormatter) Constructor(fieldName string, _ ast.Expr) string { + return "required this." + fieldName +} diff --git a/generator/format/mo/option.go b/generator/format/mo/option.go new file mode 100644 index 0000000..e46ca3a --- /dev/null +++ b/generator/format/mo/option.go @@ -0,0 +1,38 @@ +package mo + +import ( + "fmt" + "github.com/11wizards/go-to-dart/generator/format" + "go/ast" +) + +type OptionFormatter struct { +} + +func (f *OptionFormatter) under(expr ast.Expr) (format.TypeFormatter, ast.Expr) { + e := expr.(*ast.IndexExpr).Index + formatter := format.GetTypeFormatter(e) + return formatter, e +} + +func (f *OptionFormatter) CanFormat(expr ast.Expr) bool { + if x, ok := expr.(*ast.IndexExpr); ok { + if y, ok := x.X.(*ast.SelectorExpr); ok && y.X.(*ast.Ident).Name == "mo" && y.Sel.Name == "Option" { + return true + } + } + return false +} + +func (f *OptionFormatter) Signature(expr ast.Expr) string { + formatter, expr := f.under(expr) + return fmt.Sprintf("%s?", formatter.Signature(expr)) +} + +func (f *OptionFormatter) Declaration(fieldName string, expr ast.Expr) string { + return fmt.Sprintf("%s %s", f.Signature(expr), fieldName) +} + +func (f *OptionFormatter) Constructor(fieldName string, _ ast.Expr) string { + return "this." + fieldName +} diff --git a/generator/format/pointer.go b/generator/format/pointer.go new file mode 100644 index 0000000..477f59d --- /dev/null +++ b/generator/format/pointer.go @@ -0,0 +1,33 @@ +package format + +import ( + "fmt" + "go/ast" +) + +type PointerFormatter struct { +} + +func (f *PointerFormatter) under(expr ast.Expr) (TypeFormatter, ast.Expr) { + starExpr := expr.(*ast.StarExpr) + formatter := GetTypeFormatter(starExpr.X) + return formatter, starExpr.X +} + +func (f *PointerFormatter) CanFormat(expr ast.Expr) bool { + _, ok := expr.(*ast.StarExpr) + return ok +} + +func (f *PointerFormatter) Signature(expr ast.Expr) string { + formatter, expr := f.under(expr) + return fmt.Sprintf("%s?", formatter.Signature(expr)) +} + +func (f *PointerFormatter) Declaration(fieldName string, expr ast.Expr) string { + return fmt.Sprintf("%s %s", f.Signature(expr), fieldName) +} + +func (f *PointerFormatter) Constructor(fieldName string, _ ast.Expr) string { + return "this." + fieldName +} diff --git a/generator/format/primitive.go b/generator/format/primitive.go new file mode 100644 index 0000000..25df3ae --- /dev/null +++ b/generator/format/primitive.go @@ -0,0 +1,68 @@ +package format + +import ( + "fmt" + "go/ast" +) + +type PrimitiveFormatter struct { +} + +func (f *PrimitiveFormatter) toDartPrimitive(expr ast.Expr) string { + if e, ok := expr.(*ast.Ident); ok && e.Obj == nil { + switch e.Name { + case "bool": + return "bool" + case "byte": + return "int" + case "float32": + return "double" + case "float64": + return "double" + case "int": + return "int" + case "int16": + return "int" + case "int32": + return "int" + case "int64": + return "int" + case "int8": + return "int" + case "rune": + return "int" + case "string": + return "String" + case "uint": + return "int" + case "uint16": + return "int" + case "uint32": + return "int" + case "uint64": + return "int" + case "uint8": + return "int" + case "uintptr": + return "int" + } + } + + return "" +} + +func (f *PrimitiveFormatter) CanFormat(expr ast.Expr) bool { + return f.toDartPrimitive(expr) != "" +} + +func (f *PrimitiveFormatter) Signature(expr ast.Expr) string { + return f.toDartPrimitive(expr) +} + +func (f *PrimitiveFormatter) Declaration(fieldName string, expr ast.Expr) string { + return fmt.Sprintf("%s %s", f.Signature(expr), fieldName) +} + +func (f *PrimitiveFormatter) Constructor(fieldName string, _ ast.Expr) string { + return "required this." + fieldName +} diff --git a/generator/format/struct.go b/generator/format/struct.go new file mode 100644 index 0000000..cb06d74 --- /dev/null +++ b/generator/format/struct.go @@ -0,0 +1,37 @@ +package format + +import ( + "fmt" + "go/ast" +) + +type StructFormatter struct { +} + +func (f *StructFormatter) under(expr ast.Expr) *ast.TypeSpec { + if x, ok := expr.(*ast.Ident); ok && x.Obj != nil { + if y, ok := x.Obj.Decl.(*ast.TypeSpec); ok { + if _, ok := y.Type.(*ast.StructType); ok { + return y + } + } + } + return nil +} + +func (f *StructFormatter) CanFormat(expr ast.Expr) bool { + return f.under(expr) != nil +} + +func (f *StructFormatter) Signature(expr ast.Expr) string { + u := f.under(expr) + return u.Name.Name +} + +func (f *StructFormatter) Declaration(fieldName string, expr ast.Expr) string { + return fmt.Sprintf("%s %s", f.Signature(expr), fieldName) +} + +func (f *StructFormatter) Constructor(fieldName string, _ ast.Expr) string { + return "required this." + fieldName +} diff --git a/generator/format/time.go b/generator/format/time.go new file mode 100644 index 0000000..f0b69ce --- /dev/null +++ b/generator/format/time.go @@ -0,0 +1,28 @@ +package format + +import ( + "fmt" + "go/ast" +) + +type TimeFormatter struct { +} + +func (f *TimeFormatter) CanFormat(expr ast.Expr) bool { + if v, ok := expr.(*ast.SelectorExpr); ok { + return v.X.(*ast.Ident).Name == "time" && v.Sel.Name == "Time" + } + return false +} + +func (f *TimeFormatter) Signature(_ ast.Expr) string { + return "DateTime" +} + +func (f *TimeFormatter) Declaration(fieldName string, expr ast.Expr) string { + return fmt.Sprintf("%s %s", f.Signature(expr), fieldName) +} + +func (f *TimeFormatter) Constructor(fieldName string, _ ast.Expr) string { + return fmt.Sprintf("required this.%s", fieldName) +} diff --git a/generator/generator.go b/generator/generator.go new file mode 100644 index 0000000..936e768 --- /dev/null +++ b/generator/generator.go @@ -0,0 +1,87 @@ +package generator + +import ( + "bytes" + "fmt" + "github.com/11wizards/go-to-dart/generator/format" + "github.com/11wizards/go-to-dart/generator/format/mo" + "go/ast" + "go/parser" + "go/token" + "io" + "os" + "path/filepath" +) + +func init() { + format.Formatters = []format.TypeFormatter{ + &format.MapFormatter{}, + &format.ArrayFormatter{}, + &format.PointerFormatter{}, + &format.TimeFormatter{}, + &format.PrimitiveFormatter{}, + &format.AliasFormatter{}, + &format.StructFormatter{}, + &mo.OptionFormatter{}, + } +} + +func traversePackage(f *ast.Package, outputFile io.Writer) { + fmt.Fprint(outputFile, "import 'package:json_annotation/json_annotation.dart';\n\n") + fmt.Fprintf(outputFile, "part '%s.g.dart';\n\n", f.Name) + + ast.Inspect(f, func(node ast.Node) bool { + ts, ok := node.(*ast.TypeSpec) + if !ok { + return true + } + + st, ok := ts.Type.(*ast.StructType) + if !ok { + return true + } + + return generateDartClass(outputFile, ts, st) + }) +} + +func writeOut(output, outputDartFile string, wr *bytes.Buffer) { + if _, err := os.Stat(output); os.IsNotExist(err) { + err = os.MkdirAll(output, os.ModePerm) + if err != nil { + panic(err) + } + } + + outputFilePath := filepath.Join(output, outputDartFile) + outputFile, err := os.Create(outputFilePath) + if err != nil { + panic(err) + } + + defer func() { _ = outputFile.Close() }() + + _, err = outputFile.Write(wr.Bytes()) + + if err != nil { + panic(err) + } + + fmt.Printf("Processed: %s -> %s\n", outputDartFile, outputFilePath) +} + +func Run(input string, output string) { + fileSet := token.NewFileSet() + f, err := parser.ParseDir(fileSet, input, nil, 0) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + for _, pkg := range f { + var buf []byte + wr := bytes.NewBuffer(buf) + traversePackage(pkg, wr) + writeOut(output, fmt.Sprintf("%s.dart", pkg.Name), wr) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6b60f1f --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module github.com/11wizards/go-to-dart + +go 1.20 + +require ( + github.com/iancoleman/strcase v0.2.0 + github.com/openconfig/goyang v1.2.0 + github.com/samber/mo v1.8.0 + github.com/spf13/cobra v1.6.1 + github.com/stretchr/testify v1.8.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0c575f2 --- /dev/null +++ b/go.sum @@ -0,0 +1,96 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/cenkalti/backoff/v4 v4.0.0/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/protobuf v3.11.4+incompatible/go.mod h1:lUQ9D1ePzbH2PrIS7ob/bjm9HXyH5WHB0Akwh7URreM= +github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/openconfig/gnmi v0.0.0-20200414194230-1597cc0f2600/go.mod h1:M/EcuapNQgvzxo1DDXHK4tx3QpYM/uG4l591v33jG2A= +github.com/openconfig/goyang v0.0.0-20200115183954-d0a48929f0ea/go.mod h1:dhXaV0JgHJzdrHi2l+w0fZrwArtXL7jEFoiqLEdmkvU= +github.com/openconfig/goyang v1.2.0 h1:mChUZvp1kCWq6Q00wVCtOToddFzEsGlMGG+V+wNXva8= +github.com/openconfig/goyang v1.2.0/go.mod h1:vX61x01Q46AzbZUzG617vWqh/cB+aisc+RrNkXRd3W8= +github.com/openconfig/ygot v0.6.0/go.mod h1:o30svNf7O0xK+R35tlx95odkDmZWS9JyWWQSmIhqwAs= +github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/samber/mo v1.8.0 h1:vYjHTfg14JF9tD2NLhpoUsRi9bjyRoYwa4+do0nvbVw= +github.com/samber/mo v1.8.0/go.mod h1:BfkrCPuYzVG3ZljnZB783WIJIGk1mcZr9c9CPf8tAxs= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/main.go b/main.go new file mode 100644 index 0000000..6e1519e --- /dev/null +++ b/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + "github.com/11wizards/go-to-dart/generator" + "github.com/spf13/cobra" + "os" +) + +var Input string +var Output string + +var rootCmd = &cobra.Command{ + Use: "go-to-dart", + Short: "Go-to-Dart is a tool to generate Dart classes from Go structs", + Run: func(cmd *cobra.Command, args []string) { + generator.Run(Input, Output) + }, +} + +func init() { + rootCmd.PersistentFlags().StringVarP(&Input, "input", "i", "", "Input directory to read from") + rootCmd.PersistentFlags().StringVarP(&Output, "output", "o", "", "Output directory to write to") + + if err := rootCmd.MarkPersistentFlagRequired("input"); err != nil { + panic(err) + } + + if err := rootCmd.MarkPersistentFlagRequired("output"); err != nil { + panic(err) + } +} + +func main() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..655f478 --- /dev/null +++ b/main_test.go @@ -0,0 +1,53 @@ +package main + +import ( + "github.com/stretchr/testify/require" + "os" + "path" + "path/filepath" + "strings" + "testing" +) + +func runAndCompare(t *testing.T, input string) { + output := path.Join(os.TempDir(), "go-to-dart-test-output", t.Name()) + + if err := os.RemoveAll(output); err != nil { + t.Fatal(err) + return + } + + rootCmd.SetArgs([]string{"-i", input, "-o", output}) + + err := rootCmd.Execute() + require.NoError(t, err, "command failed") + + files, err := filepath.Glob(output + "**/*.dart") + require.NoError(t, err, "failed to list files") + + for _, file := range files { + relativeFile, err := filepath.Rel(output, file) + require.NoError(t, err, "failed to get relative path of %s to %s", file, output) + + expectedFile := filepath.Join(input, strings.Replace(relativeFile, ".dart.txt", ".dart", 1)) + + _, err = os.Stat(expectedFile) + require.NoError(t, err, "expected file %s to exist", expectedFile) + + actual, err := os.ReadFile(file) + require.NoError(t, err, "failed to read file %s", file) + + expected, err := os.ReadFile(expectedFile) + require.NoError(t, err, "failed to read file %s", expectedFile) + + require.Equal(t, string(expected), string(actual), "file %s does not match expected file %s", file, expectedFile) + } +} + +func TestEverything(t *testing.T) { + runAndCompare(t, "../../examples/everything") +} + +func TestUser(t *testing.T) { + runAndCompare(t, "../../examples/user") +}