diff --git a/.codeclimate.yml b/.codeclimate.yml deleted file mode 100644 index baf8a21..0000000 --- a/.codeclimate.yml +++ /dev/null @@ -1,13 +0,0 @@ -version: "2" - -exclude_patterns: -- "**/autogenerated_*" -- "**/vendor/" - -checks: - file-lines: - config: - threshold: 400 - method-lines: - config: - threshold: 100 diff --git a/.gitignore b/.gitignore index db181c1..9886722 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /go-queryset /vendor +/internal/parser/test/tmptestdir*/ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..1d70599 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,37 @@ +run: + skip-dirs: + - ^internal/parser/test/tmptestdir.* + +linters-settings: + govet: + check-shadowing: true + golint: + min-confidence: 0 + gocyclo: + min-complexity: 15 + goconst: + min-len: 2 + min-occurrences: 2 + lll: + line-length: 140 + +linters: + enable-all: true + disable: + - scopelint + - gochecknoglobals + - gosec + +issues: + exclude-rules: + - linters: + - unparam + text: always receives + - linters: + - staticcheck + text: "SA9003:" + +# golangci.com configuration +# https://github.com/golangci/golangci/wiki/Configuration +# service: +# golangci-lint-version: 1.14.0 # use fixed version to not introduce new linters unexpectedly diff --git a/.travis.yml b/.travis.yml index c0119a5..dd3f0f1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,10 @@ language: go go: - - 1.8.x - - 1.9.x + - 1.11.x + - 1.12.x before_install: - go get github.com/mattn/goveralls - - go get github.com/alecthomas/gometalinter - - go get github.com/golang/dep/cmd/dep - - dep ensure -v - - gometalinter --install --vendored-linters + - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.15.0 script: - make test - - $HOME/gopath/bin/goveralls -ignore "queryset/test/autogenerated_models.go,examples/comparison/*/*.go,queryset/test/pkgimport/*.go,queryset/test/pkgimport/*/*/*.go" -v -service=travis-ci + - $HOME/gopath/bin/goveralls -ignore "internal/queryset/generator/test/autogenerated_models.go,examples/comparison/*/*.go,internal/queryset/generator/test/pkgimport/*.go,internal/queryset/generator/test/pkgimport/*/*/*.go" -v -service=travis-ci diff --git a/Makefile b/Makefile index 1559649..1b701d9 100644 --- a/Makefile +++ b/Makefile @@ -1,24 +1,14 @@ test_static: - go install ./... - gometalinter --vendor --enable-all --min-confidence=0.3 --line-length=120 \ - -e "parameter \w+ always receives" \ - -e "/jinzhu/gorm/" \ - -e "model is unused" \ - -e '"expections" is a misspelling of "exceptions"' \ - -e "autogenerated_" \ - -e "should have comment" \ - -e "examples/comparison/" \ - --deadline=5m \ - ./... + golangci-lint run test_unit: test_gen mkdir -p test - go test -v ./parser/ ./queryset/ ./queryset/methods/ + go test -v ./... AUTOGEN_FILES = \ - ./queryset/test/autogenerated_models.go \ + ./internal/queryset/generator/test/autogenerated_models.go \ ./examples/comparison/gorm4/autogenerated_gorm4.go \ - ./queryset/test/pkgimport/autogenerated_models.go + ./internal/queryset/generator/test/pkgimport/autogenerated_models.go test_gen: gen @- $(foreach F,$(AUTOGEN_FILES), \ @@ -28,7 +18,7 @@ test_gen: gen test: test_unit bench test_static bench: - go test -bench=. -benchtime=1s -v -run=^$$ ./queryset/ + go test -bench=. -benchtime=1s -v -run=^$$ ./internal/queryset/generator/ gen: @- $(foreach F,$(AUTOGEN_FILES), \ diff --git a/cmd/goqueryset/goqueryset.go b/cmd/goqueryset/goqueryset.go index 350bf8e..8b5321c 100644 --- a/cmd/goqueryset/goqueryset.go +++ b/cmd/goqueryset/goqueryset.go @@ -1,20 +1,36 @@ package main import ( + "context" "flag" "log" - "strings" + "path/filepath" + "time" - "github.com/jirfag/go-queryset/queryset" + "github.com/jirfag/go-queryset/internal/parser" + "github.com/jirfag/go-queryset/internal/queryset/generator" ) func main() { + const defaultOutPath = "autogenerated_{in}" + inFile := flag.String("in", "models.go", "path to input file") - outFile := flag.String("out", "autogenerated_{in}", "path to output file") + outFile := flag.String("out", defaultOutPath, "path to output file") + timeout := flag.Duration("timeout", time.Minute, "timeout for generation") flag.Parse() - *outFile = strings.Replace(*outFile, "{in}", *inFile, 1) - if err := queryset.GenerateQuerySets(*inFile, *outFile); err != nil { + if *outFile == defaultOutPath { + *outFile = filepath.Join(filepath.Dir(*inFile), "autogenerated_"+filepath.Base(*inFile)) + } + + g := generator.Generator{ + StructsParser: &parser.Structs{}, + } + + ctx, finish := context.WithTimeout(context.Background(), *timeout) + defer finish() + + if err := g.Generate(ctx, *inFile, *outFile); err != nil { log.Fatalf("can't generate query sets: %s", err) } } diff --git a/parser/parser.go b/internal/parser/parser.go similarity index 57% rename from parser/parser.go rename to internal/parser/parser.go index 8609133..598ca46 100644 --- a/parser/parser.go +++ b/internal/parser/parser.go @@ -1,17 +1,19 @@ package parser import ( + "context" "fmt" "go/ast" "go/parser" "go/token" "go/types" - "os" "path/filepath" "reflect" "strings" - "golang.org/x/tools/go/loader" + "github.com/pkg/errors" + + "golang.org/x/tools/go/packages" ) // StructField represents one field in struct @@ -33,9 +35,6 @@ func (sf StructField) Tag() reflect.StructTag { return sf.tag } -// ParsedStructs is a map from struct type name to list of fields -type ParsedStructs map[string]ParsedStruct - // ParsedStruct represents struct info type ParsedStruct struct { TypeName string @@ -43,47 +42,79 @@ type ParsedStruct struct { Doc *ast.CommentGroup // line comments; or nil } -func fileNameToPkgName(filePath, absFilePath string) string { - dir := filepath.Dir(absFilePath) - gopathEnv := os.Getenv("GOPATH") - gopaths := strings.Split(gopathEnv, string(os.PathListSeparator)) - var inGoPath string - for _, gopath := range gopaths { - if strings.HasPrefix(dir, gopath) { - inGoPath = gopath - break - } - } - if inGoPath == "" { - // not in GOPATH - return "./" + filepath.Dir(filePath) - } - r := strings.TrimPrefix(dir, inGoPath) - r = strings.TrimPrefix(r, "/") // may be and may not be - r = strings.TrimPrefix(r, "\\") // may be and may not be - r = strings.TrimPrefix(r, "src/") - r = strings.TrimPrefix(r, "src\\") - return r +type Result struct { + Structs map[string]ParsedStruct + PackageName string + Types *types.Package } -func typeCheckFuncBodies(path string) bool { - return false // don't type-check func bodies to speedup parsing -} +type Structs struct{} -func loadProgramFromPackage(pkgFullName string) (*loader.Program, error) { - // The loader loads a complete Go program from source code. - conf := loader.Config{ - ParserMode: parser.ParseComments, - TypeCheckFuncBodies: typeCheckFuncBodies, +func (p Structs) ParseFile(ctx context.Context, filePath string) (*Result, error) { + absFilePath, err := filepath.Abs(filePath) + if err != nil { + return nil, errors.Wrapf(err, "can't get abs path for %s", filePath) } - conf.Import(pkgFullName) - lprog, err := conf.Load() + + neededStructs, err := p.getStructNamesInFile(absFilePath) if err != nil { - return nil, fmt.Errorf("can't load program from package %q: %s", - pkgFullName, err) + return nil, errors.Wrap(err, "can't get struct names") + } + + // need load the full package type info because + // some deps can be in other files + inPkgName := filepath.Dir(filePath) + if !filepath.IsAbs(inPkgName) && !strings.HasPrefix(inPkgName, ".") { + // to make this dir name a local package name + // can't use filepath.Join because it calls Clean and removes "."+sep + inPkgName = fmt.Sprintf(".%c%s", filepath.Separator, inPkgName) + } + + pkgs, err := packages.Load(&packages.Config{ + Mode: packages.LoadAllSyntax, + Context: ctx, + Tests: false, + }, inPkgName) + if err != nil { + return nil, errors.Wrapf(err, "failed to load package for file %s", filePath) + } + + if len(pkgs) != 1 { + return nil, fmt.Errorf("got too many (%d) packages: %#v", len(pkgs), pkgs) + } + + structs := p.buildParsedStructs(pkgs[0], neededStructs) + return &Result{ + Structs: structs, + PackageName: pkgs[0].Name, + Types: pkgs[0].Types, + }, nil +} + +func (p Structs) buildParsedStructs(pkg *packages.Package, neededStructs structNamesInfo) map[string]ParsedStruct { + ret := map[string]ParsedStruct{} + + scope := pkg.Types.Scope() + for _, name := range scope.Names() { + obj := scope.Lookup(name) + + if neededStructs[name] == nil { + continue + } + + t := obj.Type().(*types.Named) + s := t.Underlying().(*types.Struct) + + parsedStruct := parseStruct(s, neededStructs[name]) + if parsedStruct != nil { + parsedStruct.TypeName = name + ret[name] = *parsedStruct + } else { + // TODO + } } - return lprog, nil + return ret } type structNamesInfo map[string]*ast.GenDecl @@ -106,7 +137,7 @@ func (v *structNamesVisitor) Visit(n ast.Node) (w ast.Visitor) { return v } -func getStructNamesInFile(fname string) (structNamesInfo, error) { +func (p Structs) getStructNamesInFile(fname string) (structNamesInfo, error) { fset := token.NewFileSet() f, err := parser.ParseFile(fset, fname, nil, parser.ParseComments) if err != nil { @@ -120,53 +151,6 @@ func getStructNamesInFile(fname string) (structNamesInfo, error) { return v.names, nil } -// GetStructsInFile lists all structures in file passed and returns them with all fields -func GetStructsInFile(filePath string) (*loader.PackageInfo, ParsedStructs, error) { - absFilePath, err := filepath.Abs(filePath) - if err != nil { - return nil, nil, fmt.Errorf("can't get abs path for %s", filePath) - } - - neededStructs, err := getStructNamesInFile(absFilePath) - if err != nil { - return nil, nil, fmt.Errorf("can't get struct names: %s", err) - } - - packageFullName := fileNameToPkgName(filePath, absFilePath) - lprog, err := loadProgramFromPackage(packageFullName) - if err != nil { - return nil, nil, err - } - - pkgInfo := lprog.Package(packageFullName) - if pkgInfo == nil { - return nil, nil, fmt.Errorf("can't load types for file %s in package %q", - filePath, packageFullName) - } - - ret := ParsedStructs{} - - scope := pkgInfo.Pkg.Scope() - for _, name := range scope.Names() { - obj := scope.Lookup(name) - - if neededStructs[name] == nil { - continue - } - - t := obj.Type().(*types.Named) - s := t.Underlying().(*types.Struct) - - parsedStruct := parseStruct(s, neededStructs[name]) - if parsedStruct != nil { - parsedStruct.TypeName = name - ret[name] = *parsedStruct - } - } - - return pkgInfo, ret, nil -} - func newStructField(f *types.Var, tag string) *StructField { return &StructField{ name: f.Name(), diff --git a/parser/parser_test.go b/internal/parser/parser_test.go similarity index 86% rename from parser/parser_test.go rename to internal/parser/parser_test.go index 0a0c9c6..ff0fe9f 100644 --- a/parser/parser_test.go +++ b/internal/parser/parser_test.go @@ -1,6 +1,7 @@ package parser import ( + "context" "crypto/rand" "encoding/hex" "fmt" @@ -9,7 +10,6 @@ import ( "os" "path/filepath" "runtime" - "strings" "testing" "github.com/stretchr/testify/assert" @@ -30,23 +30,7 @@ func getRepoRoot() string { } func getTempDirRoot() string { - return filepath.Join(getRepoRoot(), "test") -} - -func TestFileNameToPkgName(t *testing.T) { - _, selfFilePath, _, ok := runtime.Caller(0) - assert.True(t, ok) - assert.NotEmpty(t, selfFilePath) - selfPkg := "github.com/jirfag/go-queryset/parser" - if !strings.Contains(selfFilePath, selfPkg) { - t.Skipf("it's a forked repo %q, skip pkg path test", selfFilePath) - } - - assert.Equal(t, selfPkg, fileNameToPkgName("", selfFilePath)) - - const fileNotInGoPath = "models/models.go" - const absFileNotInGoPath = "/tmp/" + fileNotInGoPath - assert.Equal(t, "./models", fileNameToPkgName(fileNotInGoPath, absFileNotInGoPath)) + return filepath.Join(getRepoRoot(), "parser", "test") } func getTempFileName(rootDir, prefix, suffix string) (*os.File, error) { @@ -123,6 +107,7 @@ func TestGetStructNamesInFile(t *testing.T) { }, } + p := Structs{} for i, tc := range cases { tc := tc // capture range variable t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { @@ -130,7 +115,7 @@ func TestGetStructNamesInFile(t *testing.T) { f := getTmpFileForCode(tc.code) defer removeTempFileAndDir(f) - res, err := getStructNamesInFile(f.Name()) + res, err := p.getStructNamesInFile(f.Name()) if tc.errorIsExpected { assert.NotNil(t, err) return @@ -267,24 +252,24 @@ func testStructFields(t *testing.T, tc structFieldsCase) { f := getTmpFileForCode(tc.code) defer removeTempFileAndDir(f) - pkg, structs, err := GetStructsInFile(f.Name()) + p := Structs{} + ret, err := p.ParseFile(context.Background(), f.Name()) if tc.errorIsExpected { assert.NotNil(t, err) return } assert.Nil(t, err) - assert.NotNil(t, pkg) - assert.NotNil(t, structs) + assert.NotNil(t, ret) - assert.Len(t, structs, tc.getExpectedtructsCount()) + assert.Len(t, ret.Structs, tc.getExpectedtructsCount()) if tc.getExpectedtructsCount() == 0 { return } var typeName string - for structTypeName := range structs { + for structTypeName := range ret.Structs { if structTypeName == "T" { typeName = structTypeName break @@ -292,7 +277,7 @@ func testStructFields(t *testing.T, tc structFieldsCase) { } assert.NotNil(t, typeName) - s := structs[typeName] + s := ret.Structs[typeName] fieldNames := []string{} for _, field := range s.Fields { assert.NotEmpty(t, field.Name) diff --git a/parser/test/test.go b/internal/parser/test/test.go similarity index 100% rename from parser/test/test.go rename to internal/parser/test/test.go diff --git a/queryset/field/field.go b/internal/queryset/field/field.go similarity index 100% rename from queryset/field/field.go rename to internal/queryset/field/field.go diff --git a/queryset/field/field_test.go b/internal/queryset/field/field_test.go similarity index 100% rename from queryset/field/field_test.go rename to internal/queryset/field/field_test.go diff --git a/internal/queryset/generator/generator.go b/internal/queryset/generator/generator.go new file mode 100644 index 0000000..7d4f6bc --- /dev/null +++ b/internal/queryset/generator/generator.go @@ -0,0 +1,103 @@ +package generator + +import ( + "bytes" + "context" + "fmt" + "io" + "log" + "os" + "path/filepath" + + "github.com/jirfag/go-queryset/internal/parser" + "github.com/pkg/errors" + "golang.org/x/tools/imports" +) + +type Generator struct { + StructsParser *parser.Structs +} + +// Generate generates output file with querysets +func (g Generator) Generate(ctx context.Context, inFilePath, outFilePath string) error { + parsedFile, err := g.StructsParser.ParseFile(ctx, inFilePath) + if err != nil { + return errors.Wrapf(err, "can't parse file %s to get structs", inFilePath) + } + + var r io.Reader + r, err = GenerateQuerySetsForStructs(parsedFile.Types, parsedFile.Structs) + if err != nil { + return errors.Wrap(err, "can't generate query sets") + } + + if r == nil { + return fmt.Errorf("no structs to generate query set in %s", inFilePath) + } + + if err = g.writeQuerySetsToOutput(r, parsedFile.PackageName, outFilePath); err != nil { + return errors.Wrapf(err, "can't save query sets to out file %s", outFilePath) + } + + var absOutPath string + absOutPath, err = filepath.Abs(outFilePath) + if err != nil { + absOutPath = outFilePath + } + + log.Printf("successfully wrote querysets to %s", absOutPath) + return nil +} + +func (g Generator) writeQuerySetsToOutput(r io.Reader, packageName, outFile string) error { + const hdrTmpl = `%s + package %s + +import ( + "errors" + "fmt" + "time" + + "github.com/jinzhu/gorm" +) +` + + // https://golang.org/s/generatedcode + const genHdr = `// Code generated by go-queryset. DO NOT EDIT.` + + var buf bytes.Buffer + pkgName := fmt.Sprintf(hdrTmpl, genHdr, packageName) + if _, err := buf.WriteString(pkgName); err != nil { + return errors.Wrap(err, "can't write hdr string into buf") + } + if _, err := io.Copy(&buf, r); err != nil { + return errors.Wrap(err, "can't write to buf") + } + + formattedRes, err := imports.Process(outFile, buf.Bytes(), nil) + if err != nil { + if os.Getenv("GOQUERYSET_DEBUG_IMPORTS_ERRORS") == "1" { + log.Printf("Can't format generated file: %s", err) + formattedRes = buf.Bytes() + } else { + return errors.Wrap(err, "can't format generated file") + } + } + + var outF *os.File + outF, err = os.OpenFile(outFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0640) // nolint: gas + if err != nil { + return fmt.Errorf("can't open out file: %s", err) + } + defer func() { + if e := outF.Close(); e != nil { + log.Printf("can't close file: %s", e) + } + }() + + if _, err = outF.Write(formattedRes); err != nil { + return errors.Wrap(err, "can't write to out file") + } + + return nil +} diff --git a/queryset/methodsbuilder.go b/internal/queryset/generator/methodsbuilder.go similarity index 95% rename from queryset/methodsbuilder.go rename to internal/queryset/generator/methodsbuilder.go index 64777a3..e0276a5 100644 --- a/queryset/methodsbuilder.go +++ b/internal/queryset/generator/methodsbuilder.go @@ -1,9 +1,9 @@ -package queryset +package generator import ( - "github.com/jirfag/go-queryset/parser" - "github.com/jirfag/go-queryset/queryset/field" - "github.com/jirfag/go-queryset/queryset/methods" + "github.com/jirfag/go-queryset/internal/parser" + "github.com/jirfag/go-queryset/internal/queryset/field" + "github.com/jirfag/go-queryset/internal/queryset/methods" ) type methodsBuilder struct { diff --git a/queryset/queryset.go b/internal/queryset/generator/queryset.go similarity index 76% rename from queryset/queryset.go rename to internal/queryset/generator/queryset.go index da71685..d9e34e0 100644 --- a/queryset/queryset.go +++ b/internal/queryset/generator/queryset.go @@ -1,18 +1,17 @@ -package queryset +package generator import ( "bytes" "fmt" "go/ast" + "go/types" "io" "sort" "strings" - "golang.org/x/tools/go/loader" - - "github.com/jirfag/go-queryset/parser" - "github.com/jirfag/go-queryset/queryset/field" - "github.com/jirfag/go-queryset/queryset/methods" + "github.com/jirfag/go-queryset/internal/parser" + "github.com/jirfag/go-queryset/internal/queryset/field" + "github.com/jirfag/go-queryset/internal/queryset/methods" ) type querySetStructConfig struct { @@ -56,8 +55,8 @@ func doesNeedToGenerateQuerySet(doc *ast.CommentGroup) bool { return false } -func genStructFieldInfos(s parser.ParsedStruct, pkgInfo *loader.PackageInfo) (ret []field.Info) { - g := field.NewInfoGenerator(pkgInfo.Pkg) +func genStructFieldInfos(s parser.ParsedStruct, types *types.Package) (ret []field.Info) { + g := field.NewInfoGenerator(types) for _, f := range s.Fields { fi := g.GenFieldInfo(f) if fi == nil { @@ -68,8 +67,8 @@ func genStructFieldInfos(s parser.ParsedStruct, pkgInfo *loader.PackageInfo) (re return ret } -func generateQuerySetConfigs(pkgInfo *loader.PackageInfo, - structs parser.ParsedStructs) querySetStructConfigSlice { +func generateQuerySetConfigs(types *types.Package, + structs map[string]parser.ParsedStruct) querySetStructConfigSlice { querySetStructConfigs := querySetStructConfigSlice{} @@ -78,7 +77,7 @@ func generateQuerySetConfigs(pkgInfo *loader.PackageInfo, continue } - fields := genStructFieldInfos(s, pkgInfo) + fields := genStructFieldInfos(s, types) b := newMethodsBuilder(s, fields) methods := b.Build() @@ -97,9 +96,8 @@ func generateQuerySetConfigs(pkgInfo *loader.PackageInfo, // GenerateQuerySetsForStructs is an internal method to retrieve querysets // generated code from parsed structs -func GenerateQuerySetsForStructs(pkgInfo *loader.PackageInfo, structs parser.ParsedStructs) (io.Reader, error) { - - querySetStructConfigs := generateQuerySetConfigs(pkgInfo, structs) +func GenerateQuerySetsForStructs(types *types.Package, structs map[string]parser.ParsedStruct) (io.Reader, error) { + querySetStructConfigs := generateQuerySetConfigs(types, structs) if len(querySetStructConfigs) == 0 { return nil, nil } diff --git a/queryset/queryset_test.go b/internal/queryset/generator/queryset_test.go similarity index 85% rename from queryset/queryset_test.go rename to internal/queryset/generator/queryset_test.go index fd7790c..9dc6e5c 100644 --- a/queryset/queryset_test.go +++ b/internal/queryset/generator/queryset_test.go @@ -1,6 +1,7 @@ -package queryset +package generator import ( + "context" "database/sql" "database/sql/driver" "fmt" @@ -15,14 +16,18 @@ import ( "testing" "time" + "github.com/jirfag/go-queryset/internal/parser" + "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" - "github.com/jirfag/go-queryset/queryset/test" - "github.com/stretchr/testify/assert" + "github.com/jirfag/go-queryset/internal/queryset/generator/test" + assert "github.com/stretchr/testify/require" - "gopkg.in/DATA-DOG/go-sqlmock.v1" + sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1" ) +const testSurname = "Ivanov" + func fixedFullRe(s string) string { return fmt.Sprintf("^%s$", regexp.QuoteMeta(s)) } @@ -139,7 +144,7 @@ func TestQueries(t *testing.T) { func testUserSelectAll(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { expUsers := getTestUsers(2) - m.ExpectQuery(fixedFullRe("SELECT * FROM `users` WHERE `users`.deleted_at IS NULL")). + m.ExpectQuery(fixedFullRe("SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL")). WillReturnRows(getRowsForUsers(expUsers)) var users []test.User @@ -149,7 +154,7 @@ func testUserSelectAll(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { func testUserSelectWithLimitAndOffset(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { expUsers := getTestUsers(2) - req := "SELECT * FROM `users` WHERE `users`.deleted_at IS NULL LIMIT 1 OFFSET 1" + req := "SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL LIMIT 1 OFFSET 1" m.ExpectQuery(fixedFullRe(req)). WillReturnRows(getRowsForUsers(expUsers)) @@ -159,7 +164,7 @@ func testUserSelectWithLimitAndOffset(t *testing.T, m sqlmock.Sqlmock, db *gorm. } func testUserSelectAllNoRecords(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { - m.ExpectQuery(fixedFullRe("SELECT * FROM `users` WHERE `users`.deleted_at IS NULL")). + m.ExpectQuery(fixedFullRe("SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL")). WillReturnError(sql.ErrNoRows) var users []test.User @@ -169,7 +174,7 @@ func testUserSelectAllNoRecords(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { func testUserSelectOne(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { expUsers := getTestUsers(1) - req := "SELECT * FROM `users` WHERE `users`.deleted_at IS NULL ORDER BY `users`.`id` ASC LIMIT 1" + req := "SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL ORDER BY `users`.`id` ASC LIMIT 1" m.ExpectQuery(fixedFullRe(req)). WillReturnRows(getRowsForUsers(expUsers)) @@ -181,11 +186,11 @@ func testUserSelectOne(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { func testUserSelectWithSurnameFilter(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { expUsers := getTestUsers(1) - surname := "Ivanov" + surname := testSurname expUsers[0].Surname = &surname req := "SELECT * FROM `users` " + - "WHERE `users`.deleted_at IS NULL AND ((user_surname = ?)) ORDER BY `users`.`id` ASC LIMIT 1" + "WHERE `users`.`deleted_at` IS NULL AND ((user_surname = ?)) ORDER BY `users`.`id` ASC LIMIT 1" m.ExpectQuery(fixedFullRe(req)). WillReturnRows(getRowsForUsers(expUsers)) @@ -242,7 +247,7 @@ func testUserQueryFilters(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { func runUserQueryFilterSubTest(t *testing.T, c userQueryTestCase, m sqlmock.Sqlmock, db *gorm.DB) { expUsers := getTestUsers(5) - req := "SELECT * FROM `users` WHERE `users`.deleted_at IS NULL AND " + c.q + req := "SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL AND " + c.q m.ExpectQuery(fixedFullRe(req)).WithArgs(c.args...). WillReturnRows(getRowsForUsers(expUsers)) @@ -270,7 +275,7 @@ func testUserCreateOneWithSurname(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) req := "INSERT INTO `users` (`created_at`,`updated_at`,`deleted_at`,`name`,`user_surname`,`email`) " + "VALUES (?,?,?,?,?,?)" - surname := "Ivanov" + surname := testSurname u.Surname = &surname args := []driver.Value{sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), @@ -284,7 +289,7 @@ func testUserCreateOneWithSurname(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) func testUserUpdateByEmail(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { u := getUser() - req := "UPDATE `users` SET `name` = ? WHERE `users`.deleted_at IS NULL AND ((email = ?))" + req := "UPDATE `users` SET `name` = ? WHERE `users`.`deleted_at` IS NULL AND ((email = ?))" m.ExpectExec(fixedFullRe(req)). WithArgs(u.Name, u.Email). WillReturnResult(sqlmock.NewResult(0, 1)) @@ -299,7 +304,7 @@ func testUserUpdateByEmail(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { func testUserUpdateFieldsByPK(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { u := getUser() - req := "UPDATE `users` SET `name` = ? WHERE `users`.deleted_at IS NULL AND `users`.`id` = ?" + req := "UPDATE `users` SET `name` = ? WHERE `users`.`deleted_at` IS NULL AND `users`.`id` = ?" m.ExpectExec(fixedFullRe(req)). WithArgs(u.Name, u.ID). WillReturnResult(sqlmock.NewResult(0, 1)) @@ -309,7 +314,7 @@ func testUserUpdateFieldsByPK(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { func testUserDeleteByEmail(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { u := getUser() - req := "UPDATE `users` SET deleted_at=? WHERE `users`.deleted_at IS NULL AND ((email = ?))" + req := "UPDATE `users` SET `deleted_at`=? WHERE `users`.`deleted_at` IS NULL AND ((email = ?))" m.ExpectExec(fixedFullRe(req)). WithArgs(sqlmock.AnyArg(), u.Email). WillReturnResult(sqlmock.NewResult(0, 1)) @@ -322,7 +327,7 @@ func testUserDeleteByEmail(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { func testUserDeleteByPK(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { u := getUser() - req := "UPDATE `users` SET deleted_at=? WHERE `users`.deleted_at IS NULL AND `users`.`id` = ?" + req := "UPDATE `users` SET `deleted_at`=? WHERE `users`.`deleted_at` IS NULL AND `users`.`id` = ?" m.ExpectExec(fixedFullRe(req)). WithArgs(sqlmock.AnyArg(), u.ID). WillReturnResult(sqlmock.NewResult(0, 1)) @@ -333,7 +338,7 @@ func testUserDeleteByPK(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { func testUsersDeleteNum(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { usersNum := 2 users := getTestUsers(usersNum) - req := "UPDATE `users` SET deleted_at=? WHERE `users`.deleted_at IS NULL AND ((email IN (?,?)))" + req := "UPDATE `users` SET `deleted_at`=? WHERE `users`.`deleted_at` IS NULL AND ((email IN (?,?)))" m.ExpectExec(fixedFullRe(req)). WithArgs(sqlmock.AnyArg(), users[0].Email, users[1].Email). WillReturnResult(sqlmock.NewResult(0, int64(usersNum))) @@ -363,7 +368,7 @@ func testUsersDeleteNumUnscoped(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { func testUsersUpdateNum(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { usersNum := 2 users := getTestUsers(usersNum) - req := "UPDATE `users` SET `name` = ? WHERE `users`.deleted_at IS NULL AND ((email IN (?,?)))" + req := "UPDATE `users` SET `name` = ? WHERE `users`.`deleted_at` IS NULL AND ((email IN (?,?)))" m.ExpectExec(fixedFullRe(req)). WithArgs(sqlmock.AnyArg(), users[0].Email, users[1].Email). WillReturnResult(sqlmock.NewResult(0, int64(usersNum))) @@ -379,7 +384,7 @@ func testUsersUpdateNum(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { func testUsersCount(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { expCount := 5 - req := "SELECT count(*) FROM `users` WHERE `users`.deleted_at IS NULL AND ((name != ?))" + req := "SELECT count(*) FROM `users` WHERE `users`.`deleted_at` IS NULL AND ((name != ?))" m.ExpectQuery(fixedFullRe(req)).WithArgs(driver.Value("")). WillReturnRows(getRowWithFields([]driver.Value{expCount})) @@ -389,7 +394,10 @@ func testUsersCount(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { } func TestMain(m *testing.M) { - err := GenerateQuerySets("test/models.go", "test/autogenerated_models.go") + g := Generator{ + StructsParser: &parser.Structs{}, + } + err := g.Generate(context.Background(), "test/models.go", "test/autogenerated_models.go") if err != nil { panic(err) } @@ -398,8 +406,12 @@ func TestMain(m *testing.M) { } func BenchmarkHello(b *testing.B) { + g := Generator{ + StructsParser: &parser.Structs{}, + } + for i := 0; i < b.N; i++ { - err := GenerateQuerySets("test/models.go", "test/autogenerated_models.go") + err := g.Generate(context.Background(), "test/models.go", "test/autogenerated_models.go") if err != nil { b.Fatalf("can't generate querysets: %s", err) } diff --git a/queryset/template.go b/internal/queryset/generator/template.go similarity index 99% rename from queryset/template.go rename to internal/queryset/generator/template.go index d0ed0af..c6d2633 100644 --- a/queryset/template.go +++ b/internal/queryset/generator/template.go @@ -1,4 +1,4 @@ -package queryset +package generator import ( "text/template" diff --git a/queryset/test/autogenerated_models.go b/internal/queryset/generator/test/autogenerated_models.go similarity index 99% rename from queryset/test/autogenerated_models.go rename to internal/queryset/generator/test/autogenerated_models.go index 48fe53a..315655b 100644 --- a/queryset/test/autogenerated_models.go +++ b/internal/queryset/generator/test/autogenerated_models.go @@ -7,7 +7,7 @@ import ( "time" "github.com/jinzhu/gorm" - "github.com/jirfag/go-queryset/queryset/tmp" + "github.com/jirfag/go-queryset/internal/queryset/generator/tmp" ) // ===== BEGIN of all query sets diff --git a/queryset/test/models.go b/internal/queryset/generator/test/models.go similarity index 86% rename from queryset/test/models.go rename to internal/queryset/generator/test/models.go index 851e690..7ec12fe 100644 --- a/queryset/test/models.go +++ b/internal/queryset/generator/test/models.go @@ -2,10 +2,10 @@ package test import ( "github.com/jinzhu/gorm" - "github.com/jirfag/go-queryset/queryset/tmp" + "github.com/jirfag/go-queryset/internal/queryset/generator/tmp" ) -//go:generate go run ../../cmd/goqueryset/goqueryset.go -in models.go +//go:generate go run ../../../../cmd/goqueryset/goqueryset.go -in models.go // User is a usual user // gen:qs diff --git a/queryset/test/pkgimport/autogenerated_models.go b/internal/queryset/generator/test/pkgimport/autogenerated_models.go similarity index 99% rename from queryset/test/pkgimport/autogenerated_models.go rename to internal/queryset/generator/test/pkgimport/autogenerated_models.go index 0545eff..7df060a 100644 --- a/queryset/test/pkgimport/autogenerated_models.go +++ b/internal/queryset/generator/test/pkgimport/autogenerated_models.go @@ -6,7 +6,7 @@ import ( "fmt" "github.com/jinzhu/gorm" - forex "github.com/jirfag/go-queryset/queryset/test/pkgimport/forex/v1" + forex "github.com/jirfag/go-queryset/internal/queryset/generator/test/pkgimport/forex/v1" ) // ===== BEGIN of all query sets diff --git a/queryset/test/pkgimport/forex/v1/types.go b/internal/queryset/generator/test/pkgimport/forex/v1/types.go similarity index 100% rename from queryset/test/pkgimport/forex/v1/types.go rename to internal/queryset/generator/test/pkgimport/forex/v1/types.go diff --git a/queryset/test/pkgimport/models.go b/internal/queryset/generator/test/pkgimport/models.go similarity index 55% rename from queryset/test/pkgimport/models.go rename to internal/queryset/generator/test/pkgimport/models.go index f296675..3cb8171 100644 --- a/queryset/test/pkgimport/models.go +++ b/internal/queryset/generator/test/pkgimport/models.go @@ -3,8 +3,8 @@ package models //go:generate goqueryset -in models.go import ( - forex "github.com/jirfag/go-queryset/queryset/test/pkgimport/forex/v1" - forexAlias "github.com/jirfag/go-queryset/queryset/test/pkgimport/forex/v1" + forex "github.com/jirfag/go-queryset/internal/queryset/generator/test/pkgimport/forex/v1" + forexAlias "github.com/jirfag/go-queryset/internal/queryset/generator/test/pkgimport/forex/v1" ) // Example is a test struct diff --git a/queryset/tmp/tmp.go b/internal/queryset/generator/tmp/tmp.go similarity index 100% rename from queryset/tmp/tmp.go rename to internal/queryset/generator/tmp/tmp.go diff --git a/queryset/methods/base.go b/internal/queryset/methods/base.go similarity index 100% rename from queryset/methods/base.go rename to internal/queryset/methods/base.go diff --git a/queryset/methods/context.go b/internal/queryset/methods/context.go similarity index 92% rename from queryset/methods/context.go rename to internal/queryset/methods/context.go index 62337df..93582f1 100644 --- a/queryset/methods/context.go +++ b/internal/queryset/methods/context.go @@ -1,8 +1,8 @@ package methods import ( - "github.com/jirfag/go-queryset/parser" - "github.com/jirfag/go-queryset/queryset/field" + "github.com/jirfag/go-queryset/internal/parser" + "github.com/jirfag/go-queryset/internal/queryset/field" ) type QsStructContext struct { diff --git a/queryset/methods/field_utils.go b/internal/queryset/methods/field_utils.go similarity index 100% rename from queryset/methods/field_utils.go rename to internal/queryset/methods/field_utils.go diff --git a/queryset/methods/field_utils_test.go b/internal/queryset/methods/field_utils_test.go similarity index 100% rename from queryset/methods/field_utils_test.go rename to internal/queryset/methods/field_utils_test.go diff --git a/queryset/methods/fields.go b/internal/queryset/methods/fields.go similarity index 100% rename from queryset/methods/fields.go rename to internal/queryset/methods/fields.go diff --git a/queryset/methods/gorm.go b/internal/queryset/methods/gorm.go similarity index 100% rename from queryset/methods/gorm.go rename to internal/queryset/methods/gorm.go diff --git a/queryset/methods/queryset.go b/internal/queryset/methods/queryset.go similarity index 94% rename from queryset/methods/queryset.go rename to internal/queryset/methods/queryset.go index 724a4c1..76d14c0 100644 --- a/queryset/methods/queryset.go +++ b/internal/queryset/methods/queryset.go @@ -272,27 +272,27 @@ func NewDeleteMethod(qsTypeName, structTypeName string) DeleteMethod { // DeleteNumMethod creates DeleteNum method type DeleteNumMethod struct { - namedMethod - baseQuerySetMethod + namedMethod + baseQuerySetMethod - noArgsMethod - constBodyMethod - constRetMethod + noArgsMethod + constBodyMethod + constRetMethod } // NewDeleteNumMethod delete row count func NewDeleteNumMethod(qsTypeName, structTypeName string) DeleteNumMethod { - return DeleteNumMethod{ - namedMethod: newNamedMethod("DeleteNum"), - baseQuerySetMethod: newBaseQuerySetMethod(qsTypeName), - constRetMethod: newConstRetMethod("(int64, error)"), - constBodyMethod: newConstBodyMethod( - strings.Join([]string{ - "db := qs.db.Delete(" + structTypeName + "{}" + ")", - "return db.RowsAffected, db.Error", - }, "\n"), - ), - } + return DeleteNumMethod{ + namedMethod: newNamedMethod("DeleteNum"), + baseQuerySetMethod: newBaseQuerySetMethod(qsTypeName), + constRetMethod: newConstRetMethod("(int64, error)"), + constBodyMethod: newConstBodyMethod( + strings.Join([]string{ + "db := qs.db.Delete(" + structTypeName + "{}" + ")", + "return db.RowsAffected, db.Error", + }, "\n"), + ), + } } // DeleteNumUnscopedMethod creates DeleteNumUnscoped method for performing hard deletes diff --git a/queryset/methods/struct.go b/internal/queryset/methods/struct.go similarity index 100% rename from queryset/methods/struct.go rename to internal/queryset/methods/struct.go diff --git a/queryset/methods/updater.go b/internal/queryset/methods/updater.go similarity index 100% rename from queryset/methods/updater.go rename to internal/queryset/methods/updater.go diff --git a/queryset/bootstrap.go b/queryset/bootstrap.go deleted file mode 100644 index b0dc254..0000000 --- a/queryset/bootstrap.go +++ /dev/null @@ -1,93 +0,0 @@ -package queryset - -import ( - "bytes" - "fmt" - "io" - "log" - "os" - "path/filepath" - - "github.com/jirfag/go-queryset/parser" - "golang.org/x/tools/go/loader" - "golang.org/x/tools/imports" -) - -// GenerateQuerySets generates output file with querysets -func GenerateQuerySets(inFilePath, outFilePath string) error { - pkgInfo, structs, err := parser.GetStructsInFile(inFilePath) - if err != nil { - return fmt.Errorf("can't parse file %s to get structs: %s", inFilePath, err) - } - - var r io.Reader - r, err = GenerateQuerySetsForStructs(pkgInfo, structs) - if err != nil { - return fmt.Errorf("can't generate query sets: %s", err) - } - - if r == nil { - return fmt.Errorf("no structs to generate query set in %s", inFilePath) - } - - if err = writeQuerySetsToOutput(r, pkgInfo, outFilePath); err != nil { - return fmt.Errorf("can't save query sets to out file %s: %s", outFilePath, err) - } - - var absOutPath string - absOutPath, err = filepath.Abs(outFilePath) - if err != nil { - absOutPath = outFilePath - } - - log.Printf("successfully wrote querysets to %s", absOutPath) - return nil -} - -func writeQuerySetsToOutput(r io.Reader, pkgInfo *loader.PackageInfo, outFile string) error { - const hdrTmpl = `%s - package %s - -import ( - "errors" - "fmt" - "time" - - "github.com/jinzhu/gorm" -) -` - - // https://golang.org/s/generatedcode - const genHdr = `// Code generated by go-queryset. DO NOT EDIT.` - - var buf bytes.Buffer - pkgName := fmt.Sprintf(hdrTmpl, genHdr, pkgInfo.Pkg.Name()) - if _, err := buf.WriteString(pkgName); err != nil { - return fmt.Errorf("can't write hdr string into buf: %s", err) - } - if _, err := io.Copy(&buf, r); err != nil { - return fmt.Errorf("can't write to buf: %s", err) - } - - formattedRes, err := imports.Process(outFile, buf.Bytes(), nil) - if err != nil { - return fmt.Errorf("can't format generated file: %s", err) - } - - var outF *os.File - outF, err = os.OpenFile(outFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0640) // nolint: gas - if err != nil { - return fmt.Errorf("can't open out file: %s", err) - } - defer func() { - if e := outF.Close(); e != nil { - log.Printf("can't close file: %s", e) - } - }() - - if _, err = outF.Write(formattedRes); err != nil { - return fmt.Errorf("can't write to out file: %s", err) - } - - return nil -} diff --git a/queryset/compile b/queryset/compile deleted file mode 100644 index 28efd77..0000000 Binary files a/queryset/compile and /dev/null differ