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  }