github.com/go-generalize/volcago@v1.7.0/generator/structGenerator.go (about) 1 package generator 2 3 import ( 4 "log" 5 "path/filepath" 6 "sort" 7 "strings" 8 "text/template" 9 10 "github.com/fatih/structtag" 11 "github.com/go-generalize/go-easyparser/types" 12 "github.com/go-generalize/volcago/pkg/fsutil" 13 "github.com/go-generalize/volcago/pkg/gocodegen" 14 "github.com/go-generalize/volcago/pkg/sliceutil" 15 "github.com/go-utils/gopackages" 16 "github.com/iancoleman/strcase" 17 "golang.org/x/xerrors" 18 ) 19 20 type structGenerator struct { 21 param templateParameter 22 23 typ *types.Object 24 baseDir string 25 structName string 26 opt GenerateOption 27 dupMap map[string]int 28 } 29 30 func newStructGenerator(typ *types.Object, structName, appVersion string, opt GenerateOption) (*structGenerator, error) { 31 g := &structGenerator{ 32 typ: typ, 33 structName: structName, 34 opt: opt, 35 dupMap: make(map[string]int), 36 } 37 38 isSameDir, err := fsutil.IsSamePath(g.baseDir, g.opt.OutputDir) 39 40 if err != nil { 41 return nil, xerrors.Errorf("failed to call IsSamePath: %w", err) 42 } 43 44 var hasMetaFields bool 45 if !opt.DisableMetaFieldsDetection { 46 hasMetaFields, err = g.hasMetaFields() 47 48 if err != nil { 49 return nil, xerrors.Errorf("meta fields are invalid: %w", err) 50 } 51 } 52 53 name := g.typ.Position.Filename 54 55 g.param.FileName = strings.TrimSuffix(filepath.Base(name), ".go") 56 g.param.GeneratedFileName = g.param.FileName + "_gen" 57 g.param.MetaFieldsEnabled = hasMetaFields 58 g.param.IsSubCollection = g.opt.Subcollection 59 60 g.param.AppVersion = appVersion 61 g.param.RepositoryInterfaceName = structName + "Repository" 62 g.param.RepositoryStructName = strcase.ToLowerCamel(g.param.RepositoryInterfaceName) 63 64 g.param.StructName = g.structName 65 g.param.StructNameRef = g.structName 66 g.param.PackageName = func() string { 67 pn := g.opt.PackageName 68 if pn == "" { 69 return g.typ.PkgName 70 } 71 return pn 72 }() 73 g.param.CollectionName = func() string { 74 cn := g.opt.CollectionName 75 if cn == "" { 76 return g.structName 77 } 78 return cn 79 }() 80 81 g.param.MockGenPath = g.opt.MockGenPath 82 g.param.MockOutputPath = func() string { 83 mop := g.opt.MockOutputPath 84 85 mop = strings.ReplaceAll(mop, "{{ .GeneratedFileName }}", g.param.GeneratedFileName) 86 if !strings.HasSuffix(mop, ".go") { 87 mop += ".go" 88 } 89 return mop 90 }() 91 92 if !isSameDir { 93 mod, err := gopackages.NewModule(g.baseDir) 94 95 if err != nil { 96 return nil, xerrors.Errorf("failed to initialize gopackages.Module: %w", err) 97 } 98 99 importPath, err := mod.GetImportPath(g.baseDir) 100 101 if err != nil { 102 return nil, xerrors.Errorf("failed to get import path for current directory: %w", err) 103 } 104 105 // Convert backslash into slash for Windows 106 importPath = filepath.ToSlash(importPath) 107 108 g.param.StructNameRef = "model." + g.structName 109 g.param.ModelImportPath = importPath 110 } 111 112 return g, nil 113 } 114 115 func isIgnoredField(tags *structtag.Tags) bool { 116 fsTag, err := tags.Get("firestore") 117 if err != nil { 118 return false 119 } 120 121 if _, err = tags.Get("firestore_key"); err == nil { 122 return false 123 } 124 125 return strings.Split(fsTag.Value(), ",")[0] == "-" 126 } 127 128 func (g *structGenerator) hasMetaFields() (bool, error) { 129 const ( 130 stringType = "string" 131 timeType = "time.Time" 132 intType = "int" 133 ) 134 135 expectedFields := map[string]struct { 136 Type string 137 }{ 138 "CreatedAt": { 139 Type: timeType, 140 }, 141 "CreatedBy": { 142 Type: stringType, 143 }, 144 "UpdatedAt": { 145 Type: timeType, 146 }, 147 "UpdatedBy": { 148 Type: stringType, 149 }, 150 "DeletedAt": { 151 Type: "*" + timeType, 152 }, 153 "DeletedBy": { 154 Type: stringType, 155 }, 156 "Version": { 157 Type: intType, 158 }, 159 } 160 161 deleted := false 162 for _, v := range g.typ.Entries { 163 matched, ok := expectedFields[v.RawName] 164 165 if !ok { 166 continue 167 } 168 169 expectedType := getGoTypeFromEPTypes(v.Type) 170 171 if expectedType != matched.Type { 172 log.Printf("%s in meta fields should be %s, but got %s", v.RawName, expectedType, matched.Type) 173 174 continue 175 } 176 177 delete(expectedFields, v.RawName) 178 deleted = true 179 } 180 181 if len(expectedFields) == 0 { 182 return true, nil 183 } 184 185 if deleted { 186 return false, xerrors.Errorf("meta fields are incomplete") 187 } 188 189 return false, nil 190 } 191 192 func (g *structGenerator) parseIndexesField(tags *structtag.Tags) error { 193 g.param.EnableIndexes = true 194 fieldInfo := &FieldInfo{ 195 FsTag: "Indexes", 196 Field: "Indexes", 197 FieldType: typeBoolMap, 198 } 199 200 tag, err := validateFirestoreTag(tags) 201 if err != nil { 202 return xerrors.Errorf("firestore tag(%s) is invalid: %w", tag, err) 203 } else if tag != "" { 204 fieldInfo.FsTag = tag 205 } 206 207 g.param.FieldInfoForIndexes = fieldInfo 208 209 return nil 210 } 211 212 func (g *structGenerator) parseType() error { 213 if err := g.parseTypeImpl("", "", g.typ); err != nil { 214 return xerrors.Errorf("failed to parse struct: %w", err) 215 } 216 217 return nil 218 } 219 220 func (g *structGenerator) parseTypeImpl(rawKey, firestoreKey string, obj *types.Object) error { 221 entries := make([]types.ObjectEntry, 0, len(obj.Entries)) 222 for _, e := range obj.Entries { 223 entries = append(entries, e) 224 } 225 226 sort.Slice(entries, func(i, j int) bool { 227 return entries[i].FieldIndex < entries[j].FieldIndex 228 }) 229 230 for _, e := range entries { 231 typeName := getGoTypeFromEPTypes(e.Type) 232 pos := e.Position.String() 233 234 if typeName == "" { 235 var o *types.Object 236 switch entry := e.Type.(type) { 237 case *types.Object: 238 o = entry 239 case *types.Nullable: 240 o = entry.Inner.(*types.Object) 241 default: 242 panic("unreachable") 243 } 244 245 fieldRawKey := strings.Join(sliceutil.RemoveEmpty([]string{rawKey, e.RawName}), ".") 246 fieldFirestoreKey := firestoreKey 247 248 tags, err := structtag.Parse(e.RawTag) 249 if err != nil { 250 fieldFirestoreKey = strings.Join(sliceutil.RemoveEmpty([]string{fieldFirestoreKey, e.RawName}), ".") 251 } else if t, err := tags.Get("firestore"); err != nil { 252 fieldFirestoreKey = strings.Join(sliceutil.RemoveEmpty([]string{fieldFirestoreKey, e.RawName}), ".") 253 } else { 254 fieldFirestoreKey = strings.Join(sliceutil.RemoveEmpty([]string{fieldFirestoreKey, t.Name}), ".") 255 } 256 257 if err = g.parseTypeImpl(fieldRawKey, fieldFirestoreKey, o); err != nil { 258 return xerrors.Errorf("failed to parse %s: %w", e.RawName, err) 259 } 260 continue 261 } 262 263 if strings.HasPrefix(typeName, "[]") { 264 g.param.SliceExist = true 265 } 266 267 tags, err := structtag.Parse(e.RawTag) 268 if err != nil { 269 log.Printf( 270 "%s: tag for %s in struct %s in %s", 271 pos, e.RawTag, g.structName, g.param.GeneratedFileName+".go", 272 ) 273 continue 274 } 275 276 if isIgnoredField(tags) { 277 continue 278 } 279 280 if rawKey == "" && e.RawName == "Indexes" && typeName == typeBoolMap { 281 if err = g.parseIndexesField(tags); err != nil { 282 return xerrors.Errorf("failed to parse indexes field: %w", err) 283 } 284 285 continue 286 } 287 288 if e.RawTag == "" { 289 fieldInfo := &FieldInfo{ 290 FsTag: strings.Join(sliceutil.RemoveEmpty([]string{firestoreKey, e.RawName}), "."), 291 Field: strings.Join(sliceutil.RemoveEmpty([]string{rawKey, e.RawName}), "."), 292 FieldType: typeName, 293 Indexes: make([]*IndexesInfo, 0), 294 } 295 if _, err = g.appendIndexer(nil, firestoreKey, fieldInfo); err != nil { 296 return xerrors.Errorf("%s: %w", pos, err) 297 } 298 g.param.FieldInfos = append(g.param.FieldInfos, fieldInfo) 299 continue 300 } 301 302 fsTag, fsTagErr := tags.Get("firestore") 303 304 tag, err := tags.Get("firestore_key") 305 if err != nil { 306 fieldInfo := &FieldInfo{ 307 FsTag: strings.Join(sliceutil.RemoveEmpty([]string{firestoreKey, e.RawName}), "."), 308 Field: strings.Join(sliceutil.RemoveEmpty([]string{rawKey, e.RawName}), "."), 309 FieldType: typeName, 310 Indexes: make([]*IndexesInfo, 0), 311 } 312 if _, err = tags.Get("unique"); err == nil { 313 if typeName != typeString { 314 return xerrors.Errorf("%s: The only field type that uses the `unique` tag is a string", pos) 315 } 316 fieldInfo.IsUnique = true 317 if g.param.UniqueInfos == nil { 318 g.param.UniqueInfos = make([]*UniqueInfo, 0) 319 } 320 ui := &UniqueInfo{ 321 Field: fieldInfo.Field, 322 FsTag: func() string { 323 ft := fieldInfo.Field 324 if fsTagErr == nil { 325 ft = strings.Split(fsTag.Value(), ",")[0] 326 } 327 if fsTagErr != nil || ft == "" { 328 return fieldInfo.Field 329 } 330 return ft 331 }(), 332 } 333 g.param.UniqueInfos = append(g.param.UniqueInfos, ui) 334 } 335 if fieldInfo, err = g.appendIndexer(tags, firestoreKey, fieldInfo); err != nil { 336 return xerrors.Errorf("%s: %w", pos, err) 337 } 338 g.param.FieldInfos = append(g.param.FieldInfos, fieldInfo) 339 continue 340 } 341 342 switch tag.Value() { 343 case "": 344 // ok 345 case "auto": 346 g.param.AutomaticGeneration = true 347 default: 348 return xerrors.Errorf(`%s: The contents of the firestore_key tag should be "" or "auto"`, pos) 349 } 350 351 // firestore タグが存在しないか-になっていない 352 if fsTagErr != nil || strings.Split(fsTag.Value(), ",")[0] != "-" { 353 return xerrors.New("key field for firestore should have firestore:\"-\" tag") 354 } 355 356 g.param.KeyFieldName = e.RawName 357 g.param.KeyFieldType = typeName 358 359 if g.param.KeyFieldType != typeString { 360 return xerrors.New("supported key types are string") 361 } 362 363 g.param.KeyValueName = strcase.ToLowerCamel(e.RawName) 364 365 // NOTE: DocumentID検索用 366 fieldInfo := &FieldInfo{ 367 Field: strings.Join(sliceutil.RemoveEmpty([]string{rawKey, e.RawName}), "."), 368 FieldType: typeName, 369 IsDocumentID: true, 370 } 371 g.param.FieldInfos = append(g.param.FieldInfos, fieldInfo) 372 } 373 374 return nil 375 } 376 377 func (g *structGenerator) generate() error { 378 templates := template.Must( 379 template.New(""). 380 Funcs(g.getFuncMap()). 381 ParseFS(templatesFS, "templates/*.tmpl"), 382 ) 383 384 gcgen := gocodegen.NewGoCodeGenerator(templates) 385 386 targets := []struct { 387 tmplName string 388 generatedName string 389 }{ 390 {"gen.go.tmpl", g.param.GeneratedFileName + ".go"}, 391 {"label.go.tmpl", g.param.FileName + "_label_gen.go"}, 392 {"constant.go.tmpl", "constant_gen.go"}, 393 {"errors.go.tmpl", "errors_gen.go"}, 394 {"misc.go.tmpl", "misc_gen.go"}, 395 {"query_builder.go.tmpl", "query_builder_gen.go"}, 396 {"query_chainer.go.tmpl", "query_chain_gen.go"}, 397 {"unique.go.tmpl", "unique_gen.go"}, 398 } 399 400 for _, t := range targets { 401 if err := gcgen.GenerateTo(t.tmplName, g.param, filepath.Join(g.opt.OutputDir, t.generatedName)); err != nil { 402 return xerrors.Errorf("failed to generate %s: %w", t.generatedName, err) 403 } 404 } 405 406 return nil 407 }