From a9f5d0902f26f32694f929feeb10469ff773a3ca Mon Sep 17 00:00:00 2001 From: Mickey Reiss Date: Tue, 18 Sep 2018 17:49:38 -0700 Subject: [PATCH] Add structs Structs are a new feature in firemodel that provide type-safety around Map aka Object aka Dictionary types. Resolves #23 and #25 --- config.go | 13 +- firemodel.example.firemodel | 9 +- internal/ast/parser.go | 10 ++ langs/go/go.go | 46 ++++++- langs/ios/ios.go | 118 +++++++++++++---- langs/ts/ts.go | 27 +++- schema.go | 123 ++++++++++++++---- schema_test.go | 59 +++++++-- .../go/test_model.firemodel.go | 4 +- .../go/test_struct.firemodel.go | 11 ++ .../swift/Firemodel.swift | 45 ++++--- .../TestFiremodelFromSchema/ts/firemodel.d.ts | 12 +- .../schema/err_ary_embedded_model.firemodel | 6 + .../schema/err_embedded_model.firemodel | 6 + testfixtures/schema/extras.firemodel | 6 +- testfixtures/schema/relational.firemodel | 1 - testfixtures/schema/struct.firemodel | 4 + 17 files changed, 399 insertions(+), 101 deletions(-) create mode 100644 testfixtures/firemodel/TestFiremodelFromSchema/go/test_struct.firemodel.go create mode 100644 testfixtures/schema/err_ary_embedded_model.firemodel create mode 100644 testfixtures/schema/err_embedded_model.firemodel create mode 100644 testfixtures/schema/struct.firemodel diff --git a/config.go b/config.go index b3975be..df3d2a7 100644 --- a/config.go +++ b/config.go @@ -10,6 +10,7 @@ import ( type Schema struct { Models []*SchemaModel Enums []*SchemaEnum + Structs []*SchemaStruct Options SchemaOptions } @@ -26,6 +27,12 @@ type SchemaModel struct { Options SchemaModelOptions } +type SchemaStruct struct { + Name string + Comment string + Fields []*SchemaField +} + type SchemaOptions map[string]map[string]string func (options SchemaOptions) Get(key string) map[string]string { @@ -125,7 +132,7 @@ type Bytes struct{} type Reference struct{ T *SchemaModel } type Array struct{ T SchemaFieldType } type Map struct{ T SchemaFieldType } -type Model struct{ T *SchemaModel } +type Struct struct{ T *SchemaStruct } type Enum struct{ T *SchemaEnum } type URL struct{} type File struct{} @@ -140,7 +147,7 @@ func (t *Bytes) isSchemaTypeName() {} func (t *Reference) isSchemaTypeName() {} func (t *Array) isSchemaTypeName() {} func (t *Map) isSchemaTypeName() {} -func (t *Model) isSchemaTypeName() {} +func (t *Struct) isSchemaTypeName() {} func (t *Enum) isSchemaTypeName() {} func (t *URL) isSchemaTypeName() {} func (t *File) isSchemaTypeName() {} @@ -148,5 +155,5 @@ func (t *File) isSchemaTypeName() {} type SchemaNestedCollection struct { Name string Comment string - Type *Model + Type *SchemaModel } diff --git a/firemodel.example.firemodel b/firemodel.example.firemodel index 7667756..72061fe 100644 --- a/firemodel.example.firemodel +++ b/firemodel.example.firemodel @@ -7,6 +7,11 @@ enum TestDirection { down, } +struct TestStruct { + string where; + integer how_much; +} + // A Test is a test model. model TestModel { option firestore.path = "users/{user_id}/test_models/{test_model_id}"; @@ -27,7 +32,7 @@ model TestModel { geopoint location; array colors; array directions; - array models; + array models; array refs; array> model_refs; map meta; @@ -35,7 +40,7 @@ model TestModel { TestDirection direction; File test_file; URL url; - TestModel nested; + TestStruct nested; collection nested_collection; } diff --git a/internal/ast/parser.go b/internal/ast/parser.go index 5133ccc..3b2b332 100644 --- a/internal/ast/parser.go +++ b/internal/ast/parser.go @@ -64,6 +64,7 @@ type ASTElement struct { Model *ASTModel `parser:" 'model' @@"` Enum *ASTEnum `parser:"| 'enum' @@"` Option *ASTOption `parser:"| 'option' @@"` + Struct *ASTStruct `parser:"| 'struct' @@"` } type ASTModel struct { @@ -71,6 +72,11 @@ type ASTModel struct { Elements []*ASTModelElement `parser:"'{' { @@ } '}'"` } +type ASTStruct struct { + Identifier ASTIdentifier `parser:"@Ident"` + Elements []*ASTStructElement `parser:"'{' { @@ } '}'"` +} + type ASTIdentifier string var ( @@ -100,6 +106,10 @@ func (id ASTIdentifier) IsReserved() bool { return true } +type ASTStructElement struct { + Field *ASTField `parser:"@@"` +} + type ASTModelElement struct { Option *ASTOption `parser:" 'option' @@"` Field *ASTField `parser:"| @@"` diff --git a/langs/go/go.go b/langs/go/go.go index e163bb4..d37906a 100644 --- a/langs/go/go.go +++ b/langs/go/go.go @@ -29,6 +29,11 @@ func (m *GoModeler) Model(schema *firemodel.Schema, sourceCoder firemodel.Source return err } } + for _, structType := range schema.Structs { + if err := m.writeStruct(structType, sourceCoder); err != nil { + return err + } + } for _, enum := range schema.Enums { if err := m.writeEnum(enum, sourceCoder); err != nil { return err @@ -60,7 +65,10 @@ func (m *GoModeler) writeModel(model *firemodel.SchemaModel, sourceCoder firemod f.Commentf("Firestore document location: /%s", fmt.Sprintf(format, commentargs...)) } - f.Type().Id(model.Name).StructFunc(m.fields(model)) + f. + Type(). + Id(model.Name). + StructFunc(m.fields(model.Name, model.Fields, model.Options.GetAutoTimestamp())) if format, args, err := model.Options.GetFirestorePath(); format != "" { f. @@ -142,6 +150,31 @@ func (m *GoModeler) writeEnum(enum *firemodel.SchemaEnum, sourceCoder firemodel. return nil } +func (m *GoModeler) writeStruct(structType *firemodel.SchemaStruct, sourceCoder firemodel.SourceCoder) error { + structName := strcase.ToCamel(structType.Name) + f := jen.NewFile(m.packageName()) + f.HeaderComment(fmt.Sprintf("DO NOT EDIT - Code generated by firemodel %s.", version.Version)) + + if structType.Comment == "" { + f.Commentf("TODO: Add comment to %s", structType.Name) + } else { + f.Comment(structType.Comment) + } + f.Type().Id(structName).StructFunc(m.fields(structName, structType.Fields, false)) + + w, err := sourceCoder.NewFile(fmt.Sprint(strcase.ToSnake(structType.Name), fileExtension)) + if err != nil { + return errors.Wrap(err, "firemodel/go: open source code file") + } + + defer w.Close() + + if err := f.Render(w); err != nil { + return err + } + return nil +} + func (m *GoModeler) packageName() string { if m.pkg == "" { return "firemodel" @@ -149,11 +182,11 @@ func (m *GoModeler) packageName() string { return m.pkg } -func (m *GoModeler) fields(model *firemodel.SchemaModel) func(g *jen.Group) { +func (m *GoModeler) fields(structName string, fields []*firemodel.SchemaField, addTimestampFields bool) func(g *jen.Group) { return func(g *jen.Group) { - for _, field := range model.Fields { + for _, field := range fields { if field.Comment == "" { - g.Commentf("TODO: Add comment to %s.%s.", model.Name, field.Name) + g.Commentf("TODO: Add comment to %s.%s.", structName, field.Name) } else { g.Comment(field.Comment) } @@ -162,8 +195,7 @@ func (m *GoModeler) fields(model *firemodel.SchemaModel) func(g *jen.Group) { Do(m.goType(field.Type)). Tag(map[string]string{"firestore": strcase.ToLowerCamel(field.Name)}) } - - if model.Options.GetAutoTimestamp() { + if addTimestampFields { g.Line() g.Comment("Creation timestamp.") g. @@ -202,7 +234,7 @@ func (m *GoModeler) goType(firetype firemodel.SchemaFieldType) func(s *jen.State return func(s *jen.Statement) { s.Op("*").Qual("cloud.google.com/go/firestore", "DocumentRef") } case *firemodel.GeoPoint: return func(s *jen.Statement) { s.Op("*").Qual("google.golang.org/genproto/googleapis/type/latlng", "LatLng") } - case *firemodel.Model: + case *firemodel.Struct: return func(s *jen.Statement) { s.Op("*").Id(firetype.T.Name) } case *firemodel.Array: if firetype.T != nil { diff --git a/langs/ios/ios.go b/langs/ios/ios.go index d4db508..a422113 100644 --- a/langs/ios/ios.go +++ b/langs/ios/ios.go @@ -2,7 +2,6 @@ package ios import ( "fmt" - "log" "strings" "text/template" @@ -35,20 +34,34 @@ var ( tpl = template.Must(template. New("file"). Funcs(map[string]interface{}{ - "firemodelVersion": func() string { return version.Version }, - "toSwiftType": toSwiftType, - "toScreamingSnake": strcase.ToScreamingSnake, - "toCamel": strcase.ToCamel, - "toLowerCamel": strcase.ToLowerCamel, - "filterFieldsEnumsOnly": filterFieldsEnumsOnly, - "firestorePath": firestorePath, + "firemodelVersion": func() string { return version.Version }, + "toSwiftType": toSwiftType, + "toScreamingSnake": strcase.ToScreamingSnake, + "toCamel": strcase.ToCamel, + "toLowerCamel": strcase.ToLowerCamel, + "filterFieldsEnumsOnly": filterFieldsEnumsOnly, + "filterFieldsNonEnumsOnly": filterFieldsNonEnumsOnly, + "filterFieldsStructsOnly": filterFieldsStructsOnly, + "hasFieldsOrStructs": hasFieldsOrStructs, + "firestorePath": firestorePath, }). Parse(file), ) _ = template.Must(tpl.New("model").Parse(model)) _ = template.Must(tpl.New("enum").Parse(enum)) + _ = template.Must(tpl.New("struct").Parse(structTpl)) ) +func hasFieldsOrStructs(in []*firemodel.SchemaField) bool { + if len(filterFieldsStructsOnly(in)) > 0 { + return true + } + if len(filterFieldsStructsOnly(in)) > 0 { + return true + } + return false +} + func filterFieldsEnumsOnly(in []*firemodel.SchemaField) []*firemodel.SchemaField { var out []*firemodel.SchemaField for _, i := range in { @@ -60,6 +73,28 @@ func filterFieldsEnumsOnly(in []*firemodel.SchemaField) []*firemodel.SchemaField return out } +func filterFieldsNonEnumsOnly(in []*firemodel.SchemaField) []*firemodel.SchemaField { + var out []*firemodel.SchemaField + for _, i := range in { + if _, ok := i.Type.(*firemodel.Enum); ok { + continue + } + out = append(out, i) + } + return out +} + +func filterFieldsStructsOnly(in []*firemodel.SchemaField) []*firemodel.SchemaField { + var out []*firemodel.SchemaField + for _, i := range in { + if _, ok := i.Type.(*firemodel.Struct); !ok { + continue + } + out = append(out, i) + } + return out +} + func toSwiftType(root bool, firetype firemodel.SchemaFieldType) string { switch firetype := firetype.(type) { case *firemodel.Boolean: @@ -69,7 +104,11 @@ func toSwiftType(root bool, firetype firemodel.SchemaFieldType) string { case *firemodel.Double: return "Float = 0.0" case *firemodel.Timestamp: - return "Date = Date()" + if root { + return "Date?" + } else { + return "Date" + } case *firemodel.URL: if root { return "URL?" @@ -115,7 +154,7 @@ func toSwiftType(root bool, firetype firemodel.SchemaFieldType) string { } else { return "Pring.File" } - case *firemodel.Model: + case *firemodel.Struct: if root { return fmt.Sprintf("%s?", firetype.T.Name) } else { @@ -131,7 +170,7 @@ func toSwiftType(root bool, firetype firemodel.SchemaFieldType) string { if firetype.T != nil { return fmt.Sprintf("[String: %s] = [:]", toSwiftType(false, firetype.T)) } else { - return "[AnyHashable: Any] = [:]" + return "[String: Any] = [:]" } default: err := errors.Errorf("firemodel/ios: unknown type %s", firetype) @@ -146,7 +185,7 @@ func firestorePath(model firemodel.SchemaModel) string { } if len(args) == 0 { - log.Printf("ios: warning: no firestore path for %s", model.Name) + fmt.Printf("warning: no firestore path for %s", model.Name) return "" } @@ -162,7 +201,7 @@ func firestorePath(model firemodel.SchemaModel) string { } path := fmt.Sprintf(format, argsWrappedInInterpolationParens...) - fmt.Fprintf(&out, " override class var path: String { return \"%s\" }\n", path) + fmt.Fprintf(&out, " override class var path: String { return \"%s\" }", path) return out.String() } @@ -175,6 +214,9 @@ import Pring {{range .Enums -}} {{template "enum" .}} {{- end}} +{{range .Structs -}} +{{template "struct" .}} +{{- end}} {{- range .Models -}} {{- template "model" .}} {{- end -}}` @@ -186,7 +228,7 @@ import Pring // TODO: Add documentation to {{.Name | toCamel}}. {{- end}} @objcMembers class {{.Name | toCamel}}: Pring.Object { - {{- . | firestorePath -}} + {{. | firestorePath}} {{- range .Fields}} {{- if .Comment}} // {{.Comment}} @@ -201,16 +243,20 @@ import Pring {{- else }} // TODO: Add documentation to {{.Name}}. {{- end}} - dynamic var {{.Name | toLowerCamel}}: Pring.NestedCollection<{{.Type.T.Name}}> = [] + dynamic var {{.Name | toLowerCamel}}: Pring.NestedCollection<{{.Type.Name}}> = [] {{- end}} - {{- if .Fields | filterFieldsEnumsOnly}} + {{- if .Fields | hasFieldsOrStructs }} override func encode(_ key: String, value: Any?) -> Any? { switch key { - {{range .Fields | filterFieldsEnumsOnly -}} + {{- range .Fields | filterFieldsEnumsOnly}} case "{{.Name | toLowerCamel}}": return self.{{.Name | toLowerCamel}}?.firestoreValue - {{- end}} + {{- end}} + {{- range .Fields | filterFieldsStructsOnly}} + case "{{.Name | toLowerCamel}}": + return self.{{.Name | toLowerCamel}}?.rawValue + {{- end}} default: break } @@ -219,10 +265,17 @@ import Pring override func decode(_ key: String, value: Any?) -> Bool { switch key { - {{range .Fields | filterFieldsEnumsOnly -}} + {{- range .Fields | filterFieldsEnumsOnly}} + case "{{.Name | toLowerCamel}}": + self.{{.Name | toLowerCamel}} = {{.Type | toSwiftType false }}(firestoreValue: value) + {{- end}} + {{- range .Fields | filterFieldsStructsOnly}} case "{{.Name | toLowerCamel}}": - self.{{.Name | toLowerCamel}} = {{.Name | toCamel }}(firestoreValue: value) - {{- end}} + if let value = value as? [String: Any] { + self.{{.Name | toLowerCamel}} = {{.Type | toSwiftType false}}(id: self.id, value: value) + return true + } + {{- end}} default: break } @@ -235,20 +288,20 @@ import Pring {{- if .Comment}} // {{.Comment}} {{- else}} -// TODO: Add documentation to {{.Name}}. +// TODO: Add documentation to {{.Name | toCamel}}. {{- end}} @objc enum {{.Name | toCamel }}: Int { {{- range .Values}} {{- if .Comment}} // {{.Comment}} {{- else}} - // TODO: Add documentation to {{.Name}}. + // TODO: Add documentation to {{.Name | toCamel}}. {{- end}} case {{.Name | toLowerCamel}} {{- end}} } -extension {{.Name}}: CustomDebugStringConvertible { +extension {{.Name | toCamel}}: CustomDebugStringConvertible { init?(firestoreValue value: Any?) { guard let value = value as? String else { return nil @@ -273,6 +326,23 @@ extension {{.Name}}: CustomDebugStringConvertible { } var debugDescription: String { return firestoreValue ?? "" } +}` + + structTpl = ` +{{- if .Comment}} +// {{.Comment}} +{{- else}} +// TODO: Add documentation to {{.Name}}. +{{- end}} +class {{.Name | toCamel }}: Pring.Object { + {{- range .Fields}} + {{- if .Comment}} + // {{.Comment}} + {{- else}} + // TODO: Add documentation to {{.Name}}. + {{- end}} + var {{.Name | toLowerCamel -}}: {{.Type | toSwiftType true}} + {{- end}} } ` ) diff --git a/langs/ts/ts.go b/langs/ts/ts.go index ecc1ee9..8cd28ad 100644 --- a/langs/ts/ts.go +++ b/langs/ts/ts.go @@ -46,6 +46,7 @@ var ( ) _ = template.Must(tpl.New("model").Parse(model)) _ = template.Must(tpl.New("enum").Parse(enum)) + _ = template.Must(tpl.New("struct").Parse(structTpl)) ) func interfaceName(sym string) string { @@ -82,7 +83,7 @@ func toTypescriptType(firetype firemodel.SchemaFieldType) string { } else { return "any[]" } - case *firemodel.Model: + case *firemodel.Struct: return interfaceName(firetype.T.Name) case *firemodel.File: return "IFile" @@ -271,6 +272,9 @@ export namespace {{.Options | getSchemaOption "ts" "namespace" "firemodel"}} { {{- range .Enums -}} {{- template "enum" .}} {{- end}} + {{- range .Structs -}} + {{- template "struct" .}} + {{- end}} {{- range .Models -}} {{- template "model" .}} {{- end}} @@ -291,7 +295,7 @@ export namespace {{.Options | getSchemaOption "ts" "namespace" "firemodel"}} { {{- else }} /** TODO: Add documentation to {{.Name}}. */ {{- end}} - {{.Name | ToLowerCamel}}: CollectionReference<{{.Type.T.Name | interfaceName | ToCamel}}>; + {{.Name | ToLowerCamel}}: CollectionReference<{{.Type.Name | interfaceName | ToCamel}}>; {{- end}} {{- range .Fields}} @@ -311,6 +315,25 @@ export namespace {{.Options | getSchemaOption "ts" "namespace" "firemodel"}} { {{- end}} }` + structTpl = ` + {{- if .Comment}} + + /** {{.Comment}} */ + {{- else}} + + /** TODO: Add documentation to {{.Name}}. */ + {{- end}} + export interface {{.Name | interfaceName | ToCamel}} { + {{- range .Fields}} + {{- if .Comment}} + /** {{.Comment}} */ + {{- else }} + /** TODO: Add documentation to {{.Name}}. */ + {{- end}} + {{.Name | ToLowerCamel -}}?: {{toTypescriptType .Type}}; + {{- end}} + }` + enum = ` {{- if .Comment}} diff --git a/schema.go b/schema.go index 99d8518..c987f0f 100644 --- a/schema.go +++ b/schema.go @@ -22,8 +22,9 @@ func ParseSchema(r io.Reader) (*Schema, error) { } type configSchemaCompiler struct { - models []*SchemaModel - enums []*SchemaEnum + models []*SchemaModel + structs []*SchemaStruct + enums []*SchemaEnum ast *ast.AST } @@ -35,10 +36,14 @@ func (c *configSchemaCompiler) compileConfig() (*Schema, error) { if err := c.precompileModelTypes(); err != nil { return nil, err } + if err := c.precompileStructTypes(); err != nil { + return nil, err + } return &Schema{ Models: c.compileModels(), Enums: c.compileEnums(), + Structs: c.compileStructs(), Options: c.compileLanguageOptions(), }, nil } @@ -81,6 +86,25 @@ func (c *configSchemaCompiler) precompileModelTypes() error { return nil } +func (c *configSchemaCompiler) precompileStructTypes() error { + c.structs = make([]*SchemaStruct, 0) + for _, v := range c.ast.Types { + if v.Struct == nil { + continue + } + + if v.Struct.Identifier.IsReserved() { + err := errors.Errorf("firemodel/schema: can't name struct %s, %s is a reserved word.", v.Struct.Identifier, v.Struct.Identifier) + return err + } + + c.structs = append(c.structs, &SchemaStruct{ + Name: strcase.ToCamel(string(v.Struct.Identifier)), + }) + } + return nil +} + func (c *configSchemaCompiler) compileModels() (out []*SchemaModel) { for _, v := range c.ast.Types { if v.Model == nil { @@ -95,7 +119,7 @@ func (c *configSchemaCompiler) compileModels() (out []*SchemaModel) { out = append(out, &SchemaModel{ Name: strcase.ToCamel(string(v.Model.Identifier)), Comment: v.Comment, - Fields: c.compileFields(v.Model.Elements), + Fields: c.compileModelFields(v.Model.Elements), Collections: c.compileCollections(v.Model.Elements), Options: c.compileModelOptions(v.Model.Elements), }) @@ -103,6 +127,26 @@ func (c *configSchemaCompiler) compileModels() (out []*SchemaModel) { return } +func (c *configSchemaCompiler) compileStructs() (out []*SchemaStruct) { + for _, v := range c.ast.Types { + if v.Struct == nil { + continue + } + + if v.Struct.Identifier.IsReserved() { + err := errors.Errorf("firemodel/schema: can't name struct %s, %s is a reserved word.", v.Struct.Identifier, v.Struct.Identifier) + panic(err) + } + + out = append(out, &SchemaStruct{ + Name: strcase.ToCamel(string(v.Struct.Identifier)), + Comment: v.Comment, + Fields: c.compileStructFields(v.Struct.Elements), + }) + } + return +} + func (c *configSchemaCompiler) compileEnums() (out []*SchemaEnum) { for _, v := range c.ast.Types { if v.Enum == nil { @@ -146,11 +190,30 @@ func (c *configSchemaCompiler) enumValuesToConfig(values []*ast.ASTEnumValue) (o return } -func (c *configSchemaCompiler) compileFields(elements []*ast.ASTModelElement) (out []*SchemaField) { +func (c *configSchemaCompiler) compileModelFields(elements []*ast.ASTModelElement) (out []*SchemaField) { for _, element := range elements { field := element.Field if field == nil { - continue // element is not a Field + continue + } + if field.Type.Base.IsCollection() { + continue // handled in compileCollections + } + + out = append(out, &SchemaField{ + Name: strcase.ToSnake(field.Name), + Comment: field.Comment, + Type: c.compileFieldType(field.Type), + }) + } + return +} + +func (c *configSchemaCompiler) compileStructFields(elements []*ast.ASTStructElement) (out []*SchemaField) { + for _, element := range elements { + field := element.Field + if field == nil { + continue } if field.Type.Base.IsCollection() { continue // handled in compileCollections @@ -193,10 +256,14 @@ func (c *configSchemaCompiler) compileFieldType(astFieldType *ast.ASTFieldType) panic("bug: enum types not yet registered") } if enum, ok := c.assertEnumType(astFieldType); ok { - return enum + return &Enum{T: enum} + } + if _, ok := c.assertModelType(astFieldType); ok { + err := errors.Errorf("firemodel/schema: can't use models as field types (got %s); please use reference, collection or switch model to struct instead", astFieldType) + panic(err) } - if model, ok := c.assertModelType(astFieldType); ok { - return model + if structT, ok := c.assertStructType(astFieldType); ok { + return &Struct{T: structT} } switch astFieldType.Base { case ast.Boolean: @@ -219,7 +286,7 @@ func (c *configSchemaCompiler) compileFieldType(astFieldType *ast.ASTFieldType) return &URL{} case ast.Map: if generic := astFieldType.Generic; generic != nil { - return &Map{T: c.compileFieldType(astFieldType.Generic)} + return &Map{T: c.compileFieldType(generic)} } return &Map{} case ast.Array: @@ -231,7 +298,7 @@ func (c *configSchemaCompiler) compileFieldType(astFieldType *ast.ASTFieldType) if astFieldType.Generic == nil { return &Reference{} } else if modelType, ok := c.assertModelType(astFieldType.Generic); ok { - return &Reference{T: modelType.T} + return &Reference{T: modelType} } else { err := errors.Errorf("firemodel: invalid generic type %s in %s<%s> (must be a model type)", astFieldType.Generic, astFieldType.Base, astFieldType.Generic) panic(err) @@ -242,37 +309,39 @@ func (c *configSchemaCompiler) compileFieldType(astFieldType *ast.ASTFieldType) panic(err) } -func (c *configSchemaCompiler) compileModelType(astType *ast.ASTFieldType) *Model { - if astType.Generic != nil { - err := errors.Errorf("models cannot have generics: %s<%s>", astType.Base, astType.Generic) - panic(err) +func (c *configSchemaCompiler) assertModelType(astFieldType *ast.ASTFieldType) (*SchemaModel, bool) { + if c.models == nil { + panic("bug: model types not yet registered") } - - if modelType, ok := c.assertModelType(astType); ok { - return modelType + if astFieldType == nil { + return nil, false } - - err := errors.Errorf("invalid type: %s", astType) - panic(err) + astType := astFieldType.Base + for _, model := range c.models { + if model.Name == strcase.ToCamel(string(astType)) { + return model, true + } + } + return nil, false } -func (c *configSchemaCompiler) assertModelType(astFieldType *ast.ASTFieldType) (*Model, bool) { - if c.models == nil { +func (c *configSchemaCompiler) assertStructType(astFieldType *ast.ASTFieldType) (*SchemaStruct, bool) { + if c.structs == nil { panic("bug: model types not yet registered") } if astFieldType == nil { return nil, false } astType := astFieldType.Base - for _, model := range c.models { - if model.Name == strcase.ToCamel(string(astType)) { - return &Model{T: model}, true + for _, schemaStruct := range c.structs { + if schemaStruct.Name == strcase.ToCamel(string(astType)) { + return schemaStruct, true } } return nil, false } -func (c *configSchemaCompiler) assertEnumType(astType *ast.ASTFieldType) (*Enum, bool) { +func (c *configSchemaCompiler) assertEnumType(astType *ast.ASTFieldType) (*SchemaEnum, bool) { if astType == nil { return nil, false } @@ -281,7 +350,7 @@ func (c *configSchemaCompiler) assertEnumType(astType *ast.ASTFieldType) (*Enum, if astType.Generic != nil { panic(errors.Errorf("generic enums are not supported: %s %v", astType.Base, astType.Generic)) } - return &Enum{T: enum}, true + return enum, true } } diff --git a/schema_test.go b/schema_test.go index da7f8bd..2dda30a 100644 --- a/schema_test.go +++ b/schema_test.go @@ -144,16 +144,16 @@ func TestParseSchema(t *testing.T) { Type: &Array{T: &String{}}, }, { - Name: "model_ary", - Type: &Array{T: &Model{ /*TestModel*/ }}, + Name: "struct_ary", + Type: &Array{T: &Reference{T: &SchemaModel{Name: "TestModel"}}}, }, { Name: "enum_ary", - Type: &Array{T: &Enum{ /*TestModel*/ }}, + Type: &Array{T: &Enum{T: &SchemaEnum{Name: "TestModel"}}}, }, { Name: "reference_ary", - Type: &Array{T: &Reference{ /* TestModel*/ }}, + Type: &Array{T: &Reference{T: &SchemaModel{Name: "TestModel"}}}, }, { Name: "nested_ary", @@ -168,12 +168,12 @@ func TestParseSchema(t *testing.T) { Type: &Map{T: &String{}}, }, { - Name: "model_map", - Type: &Map{T: &Model{ /*TestModel*/ }}, + Name: "struct_map", + Type: &Map{T: &Struct{T: &SchemaStruct{Name: "TestModel"}}}, }, { Name: "enum_map", - Type: &Map{T: &Enum{ /*TestModel*/ }}, + Type: &Map{T: &Enum{T: &SchemaEnum{Name: "TestModel"}}}, }, { Name: "generic_map", @@ -183,6 +183,11 @@ func TestParseSchema(t *testing.T) { Options: SchemaModelOptions{}, }, }, + Structs: []*SchemaStruct{ + { + Name: "TestStruct", + }, + }, Options: SchemaOptions{}, }, }, @@ -277,19 +282,14 @@ func TestParseSchema(t *testing.T) { Fields: []*SchemaField{ { Name: "owner", - Type: &Reference{ /*Operator*/ }, - }, - // note: no components "field" here. - { - Name: "embedded_component", - Type: &Model{T: &SchemaModel{Name: "Component"}}, + Type: &Reference{T: &SchemaModel{Name: "Operator"}}, }, }, Options: SchemaModelOptions{}, Collections: []*SchemaNestedCollection{ { Name: "components", - Type: &Model{ /* "Component"*/ }, + Type: &SchemaModel{Name: "Component"}, }, }, }, @@ -386,6 +386,32 @@ func TestParseSchema(t *testing.T) { name: "syntax_nonsense_2", wantErr: true, }, + { + name: "err_ary_embedded_model", + wantErr: true, + }, + { + name: "err_embedded_model", + wantErr: true, + }, + { + name: "struct", + want: &Schema{ + Structs: []*SchemaStruct{ + { + Name: "Person", + Comment: "A sample struct", + Fields: []*SchemaField{ + { + Name: "display_name", + Type: &String{}, + }, + }, + }, + }, + Options: SchemaOptions{}, + }, + }, { name: "model_named_user", want: &Schema{ @@ -408,6 +434,11 @@ func TestParseSchema(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + defer func() { + if p := recover(); p != nil && !tt.wantErr { + t.Fatal("panic", p) + } + }() r, err := os.Open(path.Join("testfixtures", "schema", tt.name+".firemodel")) if err != nil { t.Fatal(err) diff --git a/testfixtures/firemodel/TestFiremodelFromSchema/go/test_model.firemodel.go b/testfixtures/firemodel/TestFiremodelFromSchema/go/test_model.firemodel.go index 05307ef..b3f6833 100644 --- a/testfixtures/firemodel/TestFiremodelFromSchema/go/test_model.firemodel.go +++ b/testfixtures/firemodel/TestFiremodelFromSchema/go/test_model.firemodel.go @@ -35,7 +35,7 @@ type TestModel struct { // TODO: Add comment to TestModel.directions. Directions []TestDirection `firestore:"directions"` // TODO: Add comment to TestModel.models. - Models []*TestModel `firestore:"models"` + Models []*TestStruct `firestore:"models"` // TODO: Add comment to TestModel.refs. Refs []*firestore.DocumentRef `firestore:"refs"` // TODO: Add comment to TestModel.model_refs. @@ -51,7 +51,7 @@ type TestModel struct { // TODO: Add comment to TestModel.url. Url runtime.URL `firestore:"url"` // TODO: Add comment to TestModel.nested. - Nested *TestModel `firestore:"nested"` + Nested *TestStruct `firestore:"nested"` // Creation timestamp. CreatedAt time.Time `firestore:"createdAt,serverTimestamp"` diff --git a/testfixtures/firemodel/TestFiremodelFromSchema/go/test_struct.firemodel.go b/testfixtures/firemodel/TestFiremodelFromSchema/go/test_struct.firemodel.go new file mode 100644 index 0000000..515dbfd --- /dev/null +++ b/testfixtures/firemodel/TestFiremodelFromSchema/go/test_struct.firemodel.go @@ -0,0 +1,11 @@ +// DO NOT EDIT - Code generated by firemodel (dev). + +package firemodel + +// TODO: Add comment to TestStruct +type TestStruct struct { + // TODO: Add comment to TestStruct.where. + Where string `firestore:"where"` + // TODO: Add comment to TestStruct.how_much. + HowMuch int64 `firestore:"howMuch"` +} diff --git a/testfixtures/firemodel/TestFiremodelFromSchema/swift/Firemodel.swift b/testfixtures/firemodel/TestFiremodelFromSchema/swift/Firemodel.swift index f64822b..0f062cd 100644 --- a/testfixtures/firemodel/TestFiremodelFromSchema/swift/Firemodel.swift +++ b/testfixtures/firemodel/TestFiremodelFromSchema/swift/Firemodel.swift @@ -5,13 +5,13 @@ import Pring // TODO: Add documentation to TestDirection. @objc enum TestDirection: Int { - // TODO: Add documentation to left. + // TODO: Add documentation to Left. case left - // TODO: Add documentation to right. + // TODO: Add documentation to Right. case right - // TODO: Add documentation to up. + // TODO: Add documentation to Up. case up - // TODO: Add documentation to down. + // TODO: Add documentation to Down. case down } @@ -50,11 +50,19 @@ extension TestDirection: CustomDebugStringConvertible { var debugDescription: String { return firestoreValue ?? "" } } +// TODO: Add documentation to TestStruct. +class TestStruct: Pring.Object { + // TODO: Add documentation to where. + var where: String? + // TODO: Add documentation to how_much. + var howMuch: Int = 0 +} + // A Test is a test model. -@objcMembers class TestModel: Pring.Object {static var userId: String = "" +@objcMembers class TestModel: Pring.Object { + static var userId: String = "" static var testModelId: String = "" - override class var path: String { return "users/\(userId)/test_models/\(testModelId)" } - + override class var path: String { return "users/\(userId)/test_models/\(testModelId)" } // The name. dynamic var name: String? // The age. @@ -62,7 +70,7 @@ static var testModelId: String = "" // The number pi. dynamic var pi: Float = 0.0 // The birth date. - dynamic var birthdate: Date = Date() + dynamic var birthdate: Date? // True if it is good. dynamic var isGood: Bool = false // TODO: Add documentation to data. @@ -76,13 +84,13 @@ static var testModelId: String = "" // TODO: Add documentation to directions. dynamic var directions: [TestDirection]? // TODO: Add documentation to models. - dynamic var models: [TestModel]? + dynamic var models: [TestStruct]? // TODO: Add documentation to refs. dynamic var refs: [Pring.AnyReference]? // TODO: Add documentation to modelRefs. dynamic var modelRefs: [Pring.Reference]? // TODO: Add documentation to meta. - dynamic var meta: [AnyHashable: Any] = [:] + dynamic var meta: [String: Any] = [:] // TODO: Add documentation to metaStrs. dynamic var metaStrs: [String: String] = [:] // TODO: Add documentation to direction. @@ -92,7 +100,7 @@ static var testModelId: String = "" // TODO: Add documentation to url. dynamic var url: URL? // TODO: Add documentation to nested. - dynamic var nested: TestModel? + dynamic var nested: TestStruct? // TODO: Add documentation to nested_collection. dynamic var nestedCollection: Pring.NestedCollection = [] @@ -100,6 +108,8 @@ static var testModelId: String = "" switch key { case "direction": return self.direction?.firestoreValue + case "nested": + return self.nested?.rawValue default: break } @@ -109,7 +119,12 @@ static var testModelId: String = "" override func decode(_ key: String, value: Any?) -> Bool { switch key { case "direction": - self.direction = Direction(firestoreValue: value) + self.direction = TestDirection(firestoreValue: value) + case "nested": + if let value = value as? [String: Any] { + self.nested = TestStruct(id: self.id, value: value) + return true + } default: break } @@ -118,7 +133,7 @@ static var testModelId: String = "" } // TODO: Add documentation to TestTimestamps. -@objcMembers class TestTimestamps: Pring.Object {static var testTimestampsId: String = "" - override class var path: String { return "timestamps/\(testTimestampsId)" } - +@objcMembers class TestTimestamps: Pring.Object { + static var testTimestampsId: String = "" + override class var path: String { return "timestamps/\(testTimestampsId)" } } diff --git a/testfixtures/firemodel/TestFiremodelFromSchema/ts/firemodel.d.ts b/testfixtures/firemodel/TestFiremodelFromSchema/ts/firemodel.d.ts index bef7c0f..4d5eaad 100644 --- a/testfixtures/firemodel/TestFiremodelFromSchema/ts/firemodel.d.ts +++ b/testfixtures/firemodel/TestFiremodelFromSchema/ts/firemodel.d.ts @@ -145,6 +145,14 @@ export namespace example { down = "DOWN", } + /** TODO: Add documentation to TestStruct. */ + export interface ITestStruct { + /** TODO: Add documentation to where. */ + where?: string; + /** TODO: Add documentation to how_much. */ + howMuch?: number; + } + /** A Test is a test model. */ export interface ITestModel { /** TODO: Add documentation to nested_collection. */ @@ -170,7 +178,7 @@ export namespace example { /** TODO: Add documentation to directions. */ directions?: TestDirection[]; /** TODO: Add documentation to models. */ - models?: ITestModel[]; + models?: ITestStruct[]; /** TODO: Add documentation to refs. */ refs?: firestore.DocumentReference[]; /** TODO: Add documentation to model_refs. */ @@ -186,7 +194,7 @@ export namespace example { /** TODO: Add documentation to url. */ url?: URL; /** TODO: Add documentation to nested. */ - nested?: ITestModel; + nested?: ITestStruct; /** Record creation timestamp. */ createdAt?: firestore.Timestamp; diff --git a/testfixtures/schema/err_ary_embedded_model.firemodel b/testfixtures/schema/err_ary_embedded_model.firemodel new file mode 100644 index 0000000..a21046e --- /dev/null +++ b/testfixtures/schema/err_ary_embedded_model.firemodel @@ -0,0 +1,6 @@ +model SomeModel {} + +model Invalid { + // Invalid! + array bad_nested_ary; +} diff --git a/testfixtures/schema/err_embedded_model.firemodel b/testfixtures/schema/err_embedded_model.firemodel new file mode 100644 index 0000000..ceb5721 --- /dev/null +++ b/testfixtures/schema/err_embedded_model.firemodel @@ -0,0 +1,6 @@ +model SomeModel {} + +model Invalid { + // Invalid! + TestModel bad_nested; +} diff --git a/testfixtures/schema/extras.firemodel b/testfixtures/schema/extras.firemodel index df6c4e2..9cde9d5 100644 --- a/testfixtures/schema/extras.firemodel +++ b/testfixtures/schema/extras.firemodel @@ -1,16 +1,18 @@ enum TestEnum {} +struct TestStruct {} + model TestModel { reference other; reference unspecified_other; array primative_ary; - array model_ary; + array struct_ary; array enum_ary; array> reference_ary; array> nested_ary; array generic_ary; map primative_map; - map model_map; + map struct_map; map enum_map; map generic_map; } diff --git a/testfixtures/schema/relational.firemodel b/testfixtures/schema/relational.firemodel index 507ee43..80f9cde 100644 --- a/testfixtures/schema/relational.firemodel +++ b/testfixtures/schema/relational.firemodel @@ -7,5 +7,4 @@ model Component { model Machine { reference owner; collection components; - Component embedded_component; } diff --git a/testfixtures/schema/struct.firemodel b/testfixtures/schema/struct.firemodel new file mode 100644 index 0000000..0340d40 --- /dev/null +++ b/testfixtures/schema/struct.firemodel @@ -0,0 +1,4 @@ +// A sample struct +struct Person { + string display_name; +}