diff --git a/v2/pkg/parser/conversion.go b/v2/pkg/parser/conversion.go index c55d7037..6d12a6d3 100644 --- a/v2/pkg/parser/conversion.go +++ b/v2/pkg/parser/conversion.go @@ -1,5 +1,12 @@ package parser +import ( + "fmt" + "strings" + + "github.com/leaanthony/slicer" +) + // JSType represents a javascript type type JSType string @@ -37,7 +44,7 @@ func goTypeToJS(input *Field) string { case "struct": return input.Struct.Name default: - println("UNSUPPORTED: ", input) + fmt.Printf("Unsupported input to goTypeToJS: %+v", input) return "*" } } @@ -71,7 +78,7 @@ func goTypeToTS(input *Field, pkgName string) string { // case reflect.Map, reflect.Interface: // return string(JsObject) default: - println("UNSUPPORTED: ", input) + fmt.Printf("Unsupported input to goTypeToTS: %+v", input) return JsUnsupported } @@ -81,3 +88,32 @@ func goTypeToTS(input *Field, pkgName string) string { return result } + +func isUnresolvedType(typeName string) bool { + switch typeName { + case "string": + return false + case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64": + return false + case "float32", "float64": + return false + case "bool": + return false + case "struct": + return false + default: + return true + } +} + +var reservedJSWords []string = []string{"abstract", "arguments", "await", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", "debugger", "default", "delete", "do", "double", "else", "enum", "eval", "export", "extends", "false", "final", "finally", "float", "for", "function", "goto", "if", "implements", "import", "in", "instanceof", "int", "interface", "let", "long", "native", "new", "null", "package", "private", "protected", "public", "return", "short", "static", "super", "switch", "synchronized", "this", "throw", "throws", "transient", "true", "try", "typeof", "var", "void", "volatile", "while", "with", "yield", "Array", "Date", "eval", "function", "hasOwnProperty", "Infinity", "isFinite", "isNaN", "isPrototypeOf", "length", "Math", "NaN", "Number", "Object", "prototype", "String", "toString", "undefined", "valueOf"} +var jsReservedWords *slicer.StringSlicer = slicer.String(reservedJSWords) + +func isJSReservedWord(input string) bool { + return jsReservedWords.Contains(input) +} + +func startsWithLowerCaseLetter(input string) bool { + firstLetter := string(input[0]) + return strings.ToLower(firstLetter) == firstLetter +} diff --git a/v2/pkg/parser/field.go b/v2/pkg/parser/field.go index 6f93a517..f8908074 100644 --- a/v2/pkg/parser/field.go +++ b/v2/pkg/parser/field.go @@ -3,8 +3,10 @@ package parser import ( "fmt" "go/ast" + "strings" "github.com/davecgh/go-spew/spew" + "github.com/fatih/structtag" ) // Field defines a parsed struct field @@ -25,6 +27,15 @@ type Field struct { // Indicates if the Field is an array of type "Type" IsArray bool + + // JSON field name defined by a json tag + JSONOptions +} + +type JSONOptions struct { + Name string + IsOptional bool + Ignored bool } // JSType returns the Javascript type for this field @@ -32,6 +43,31 @@ func (f *Field) JSType() string { return string(goTypeToJS(f)) } +// JSName returns the Javascript name for this field +func (f *Field) JSName() string { + if f.JSONOptions.Name != "" { + return f.JSONOptions.Name + } + return f.Name +} + +// NameForPropertyDoc returns a formatted name for the jsdoc @property declaration +func (f *Field) NameForPropertyDoc() string { + if f.IsOptional { + return "[" + f.JSName() + "]" + } + return f.JSName() +} + +// TypeForPropertyDoc returns a formatted name for the jsdoc @property declaration +func (f *Field) TypeForPropertyDoc() string { + result := goTypeToJS(f) + if f.IsArray { + result += "[]" + } + return result +} + // TypeAsTSType converts the Field type to something TS wants func (f *Field) TypeAsTSType(pkgName string) string { var result = "" @@ -65,10 +101,36 @@ func (p *Parser) parseField(file *ast.File, field *ast.Field, pkg *Package) ([]* var strct *Struct var isArray bool + var jsonOptions JSONOptions + // Determine type switch t := field.Type.(type) { case *ast.Ident: fieldType = t.Name + + unresolved := isUnresolvedType(fieldType) + + // Check if this type is actually a struct + if unresolved { + // Assume it is a struct + // Parse the struct + var err error + strct, err = p.parseStruct(pkg, t.Name) + if err != nil { + return nil, err + } + + if strct == nil { + fieldName := "" + if len(field.Names) > 0 { + fieldName = field.Names[0].Name + } + return nil, fmt.Errorf("unresolved type in field %s: %s", fieldName, fieldType) + } + + fieldType = "struct" + + } case *ast.StarExpr: fieldType = "struct" packageName, structName, err := parseStructNameFromStarExpr(t) @@ -150,10 +212,24 @@ func (p *Parser) parseField(file *ast.File, field *ast.Field, pkg *Package) ([]* return nil, fmt.Errorf("unsupported field found in struct: %+v", t) } + // Parse json tag if available + if field.Tag != nil { + err := parseJSONOptions(field.Tag.Value, &jsonOptions) + if err != nil { + return nil, err + } + } + // Loop over names if we have if len(field.Names) > 0 { + for _, name := range field.Names { + // TODO: Check field names are valid in JS + if isJSReservedWord(name.Name) { + return nil, fmt.Errorf("unable to use field name %s - reserved word in Javascript", name.Name) + } + // Create a field per name thisField := &Field{ Comments: parseComments(field.Doc), @@ -162,6 +238,7 @@ func (p *Parser) parseField(file *ast.File, field *ast.Field, pkg *Package) ([]* thisField.Type = fieldType thisField.Struct = strct thisField.IsArray = isArray + thisField.JSONOptions = jsonOptions result = append(result, thisField) } @@ -179,3 +256,39 @@ func (p *Parser) parseField(file *ast.File, field *ast.Field, pkg *Package) ([]* return result, nil } + +func parseJSONOptions(fieldTag string, jsonOptions *JSONOptions) error { + + // Remove backticks + fieldTag = strings.Trim(fieldTag, "`") + + // Parse the tag + tags, err := structtag.Parse(fieldTag) + if err != nil { + return err + } + + jsonTag, err := tags.Get("json") + if err != nil { + return err + } + + if jsonTag == nil { + return nil + } + + // Save the name + jsonOptions.Name = jsonTag.Name + + // Check if this field is ignored + if jsonTag.Name == "-" { + jsonOptions.Ignored = true + } + + // Check if this field is optional + if jsonTag.HasOption("omitempty") { + jsonOptions.IsOptional = true + } + + return nil +} diff --git a/v2/pkg/parser/globals.d.ts b/v2/pkg/parser/globals.d.ts index 522851a9..4c441632 100644 --- a/v2/pkg/parser/globals.d.ts +++ b/v2/pkg/parser/globals.d.ts @@ -1,6 +1,6 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT -interface window { +interface Window { backend: any } \ No newline at end of file diff --git a/v2/pkg/parser/package.template b/v2/pkg/parser/package.template index 332adc1f..2c7362df 100644 --- a/v2/pkg/parser/package.template +++ b/v2/pkg/parser/package.template @@ -1,24 +1,27 @@ +// @ts-check // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT +{{- if .DeclarationReferences }} +{{range .DeclarationReferences}} +const {{.}} = require('./_{{.}}');{{end}}{{- end}} + {{- range $struct := .Structs }} {{- if .IsUsedAsData }} + /** {{if .Comments }}{{range .Comments}} *{{ . }}{{end}}{{end}} * @typedef {object} {{.Name}} -{{range .Fields}} * @property {{"{"}}{{.JSType}}{{"}"}} {{.Name}} -{{- if .Comments}} - {{- range .Comments}}{{ . }}{{- end}}{{- end}} -{{end}} * - */ -export const {{.Name}} = { -{{- range .Fields}} - {{.Name}}, +{{- range .Fields}}{{- if not .JSONOptions.Ignored }} + * @property {{"{"}}{{.TypeForPropertyDoc}}{{"}"}} {{.NameForPropertyDoc}} {{- if .Comments}} - {{- range .Comments}}{{ . }}{{- end}}{{- end}}{{- end}} {{- end}} -} + */ +export var {{.Name}}; {{- end}} {{- if .IsBound }} -{{if .Methods }} +{{- if .Methods }} + {{if .Comments }}{{range .Comments}}// {{ . }}{{end}}{{end}} export const {{.Name}} = { {{range .Methods }} @@ -35,9 +38,7 @@ export const {{.Name}} = { }, {{end}} } -{{end}} + +{{- end}} {{- end}} {{- end}} - - - diff --git a/v2/pkg/parser/parseStructFields.go b/v2/pkg/parser/parseStructFields.go index f726f3ef..cdca538d 100644 --- a/v2/pkg/parser/parseStructFields.go +++ b/v2/pkg/parser/parseStructFields.go @@ -1,6 +1,10 @@ package parser -import "go/ast" +import ( + "go/ast" + + "github.com/pkg/errors" +) func (p *Parser) parseStructFields(fileAst *ast.File, structType *ast.StructType, boundStruct *Struct) error { @@ -8,7 +12,7 @@ func (p *Parser) parseStructFields(fileAst *ast.File, structType *ast.StructType for _, field := range structType.Fields.List { fields, err := p.parseField(fileAst, field, boundStruct.Package) if err != nil { - return err + return errors.Wrap(err, "error parsing struct "+boundStruct.Name) } // If this field was a struct, flag that it is used as data @@ -18,7 +22,13 @@ func (p *Parser) parseStructFields(fileAst *ast.File, structType *ast.StructType } } - boundStruct.Fields = append(boundStruct.Fields, fields...) + // If this field name is lowercase, it won't be exported + for _, field := range fields { + if !startsWithLowerCaseLetter(field.Name) { + boundStruct.Fields = append(boundStruct.Fields, field) + } + } + } return nil