diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cde7250..5207bf6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,7 +7,7 @@ on: branches: [ main ] jobs: - build: + lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index 71b2d43..8bdc515 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,7 @@ Repogen is a code generator for database repository in Golang. (WIP) + +## Features + +Repogen is a library that generates MongoDB repository implementation from repository interface by using method name pattern. diff --git a/internal/mongo/generator.go b/internal/mongo/generator.go index b3e43ea..3b9e705 100644 --- a/internal/mongo/generator.go +++ b/internal/mongo/generator.go @@ -120,24 +120,24 @@ func (g mongoRepositoryGenerator) generateMethodImplementation(methodSpec spec.M func (g mongoRepositoryGenerator) generateFindImplementation(operation spec.FindOperation) (string, error) { buffer := new(bytes.Buffer) - var queryFields []string - for _, fieldName := range operation.Query.Fields { - structField, ok := g.StructModel.Fields.ByName(fieldName) + var predicates []predicate + for _, predicateSpec := range operation.Query.Predicates { + structField, ok := g.StructModel.Fields.ByName(predicateSpec.Field) if !ok { - return "", fmt.Errorf("struct field %s not found", fieldName) + return "", fmt.Errorf("struct field %s not found", predicateSpec.Field) } bsonTag, ok := structField.Tags["bson"] if !ok { - return "", fmt.Errorf("struct field %s does not have bson tag", fieldName) + return "", fmt.Errorf("struct field %s does not have bson tag", predicateSpec.Field) } - queryFields = append(queryFields, bsonTag[0]) + predicates = append(predicates, predicate{Field: bsonTag[0], Operator: predicateSpec.Operator}) } tmplData := mongoFindTemplateData{ - EntityType: g.StructModel.Name, - QueryFields: queryFields, + EntityType: g.StructModel.Name, + Predicates: predicates, } if operation.Mode == spec.QueryModeOne { diff --git a/internal/mongo/generator_test.go b/internal/mongo/generator_test.go index 0a77702..87e1486 100644 --- a/internal/mongo/generator_test.go +++ b/internal/mongo/generator_test.go @@ -22,6 +22,16 @@ func TestGenerateMongoRepository(t *testing.T) { Type: code.SimpleType("string"), Tags: map[string][]string{"bson": {"username"}}, }, + { + Name: "Gender", + Type: code.SimpleType("Gender"), + Tags: map[string][]string{"bson": {"gender"}}, + }, + { + Name: "Age", + Type: code.SimpleType("int"), + Tags: map[string][]string{"bson": {"age"}}, + }, }, } intf := code.Interface{ @@ -69,6 +79,61 @@ func TestGenerateMongoRepository(t *testing.T) { code.SimpleType("error"), }, }, + { + Name: "FindByGenderNot", + Params: []code.Param{ + {Name: "ctx", Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Name: "gender", Type: code.SimpleType("int")}, + }, + Returns: []code.Type{ + code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, + code.SimpleType("error"), + }, + }, + { + Name: "FindByAgeLessThan", + Params: []code.Param{ + {Name: "ctx", Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Name: "age", Type: code.SimpleType("int")}, + }, + Returns: []code.Type{ + code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, + code.SimpleType("error"), + }, + }, + { + Name: "FindByAgeLessThanEqual", + Params: []code.Param{ + {Name: "ctx", Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Name: "age", Type: code.SimpleType("int")}, + }, + Returns: []code.Type{ + code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, + code.SimpleType("error"), + }, + }, + { + Name: "FindByAgeGreaterThan", + Params: []code.Param{ + {Name: "ctx", Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Name: "age", Type: code.SimpleType("int")}, + }, + Returns: []code.Type{ + code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, + code.SimpleType("error"), + }, + }, + { + Name: "FindByAgeGreaterThanEqual", + Params: []code.Param{ + {Name: "ctx", Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Name: "age", Type: code.SimpleType("int")}, + }, + Returns: []code.Type{ + code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, + code.SimpleType("error"), + }, + }, }, } @@ -142,6 +207,76 @@ func (r *UserRepositoryMongo) FindByIDAndUsername(ctx context.Context, arg0 prim } return &entity, nil } + +func (r *UserRepositoryMongo) FindByGenderNot(ctx context.Context, arg0 int) ([]*UserModel, error) { + cursor, err := r.collection.Find(ctx, bson.M{ + "gender": bson.M{"$ne": arg0}, + }) + if err != nil { + return nil, err + } + var entities []*UserModel + if err := cursor.All(ctx, &entities); err != nil { + return nil, err + } + return entities, nil +} + +func (r *UserRepositoryMongo) FindByAgeLessThan(ctx context.Context, arg0 int) ([]*UserModel, error) { + cursor, err := r.collection.Find(ctx, bson.M{ + "age": bson.M{"$lt": arg0}, + }) + if err != nil { + return nil, err + } + var entities []*UserModel + if err := cursor.All(ctx, &entities); err != nil { + return nil, err + } + return entities, nil +} + +func (r *UserRepositoryMongo) FindByAgeLessThanEqual(ctx context.Context, arg0 int) ([]*UserModel, error) { + cursor, err := r.collection.Find(ctx, bson.M{ + "age": bson.M{"$lte": arg0}, + }) + if err != nil { + return nil, err + } + var entities []*UserModel + if err := cursor.All(ctx, &entities); err != nil { + return nil, err + } + return entities, nil +} + +func (r *UserRepositoryMongo) FindByAgeGreaterThan(ctx context.Context, arg0 int) ([]*UserModel, error) { + cursor, err := r.collection.Find(ctx, bson.M{ + "age": bson.M{"$gt": arg0}, + }) + if err != nil { + return nil, err + } + var entities []*UserModel + if err := cursor.All(ctx, &entities); err != nil { + return nil, err + } + return entities, nil +} + +func (r *UserRepositoryMongo) FindByAgeGreaterThanEqual(ctx context.Context, arg0 int) ([]*UserModel, error) { + cursor, err := r.collection.Find(ctx, bson.M{ + "age": bson.M{"$gte": arg0}, + }) + if err != nil { + return nil, err + } + var entities []*UserModel + if err := cursor.All(ctx, &entities); err != nil { + return nil, err + } + return entities, nil +} ` expectedCodeLines := strings.Split(expectedCode, "\n") actualCodeLines := strings.Split(code, "\n") diff --git a/internal/mongo/models.go b/internal/mongo/models.go new file mode 100644 index 0000000..ca07421 --- /dev/null +++ b/internal/mongo/models.go @@ -0,0 +1,30 @@ +package mongo + +import ( + "fmt" + + "github.com/sunboyy/repogen/internal/spec" +) + +type predicate struct { + Field string + Operator spec.Operator +} + +func (p predicate) Code(argIndex int) string { + switch p.Operator { + case spec.OperatorEqual: + return fmt.Sprintf(`"%s": arg%d`, p.Field, argIndex) + case spec.OperatorNot: + return fmt.Sprintf(`"%s": bson.M{"$ne": arg%d}`, p.Field, argIndex) + case spec.OperatorLessThan: + return fmt.Sprintf(`"%s": bson.M{"$lt": arg%d}`, p.Field, argIndex) + case spec.OperatorLessThanEqual: + return fmt.Sprintf(`"%s": bson.M{"$lte": arg%d}`, p.Field, argIndex) + case spec.OperatorGreaterThan: + return fmt.Sprintf(`"%s": bson.M{"$gt": arg%d}`, p.Field, argIndex) + case spec.OperatorGreaterThanEqual: + return fmt.Sprintf(`"%s": bson.M{"$gte": arg%d}`, p.Field, argIndex) + } + return "" +} diff --git a/internal/mongo/templates.go b/internal/mongo/templates.go index 41b10cf..e421a61 100644 --- a/internal/mongo/templates.go +++ b/internal/mongo/templates.go @@ -75,19 +75,19 @@ func (data mongoMethodTemplateData) Returns() string { const findOneTemplate = ` var entity {{.EntityType}} if err := r.collection.FindOne(ctx, bson.M{ -{{range $index, $field := .QueryFields}} "{{$field}}": arg{{$index}}, +{{range $index, $field := .Predicates}} {{$field.Code $index}}, {{end}} }).Decode(&entity); err != nil { return nil, err } return &entity, nil` type mongoFindTemplateData struct { - EntityType string - QueryFields []string + EntityType string + Predicates []predicate } const findManyTemplate = ` cursor, err := r.collection.Find(ctx, bson.M{ -{{range $index, $field := .QueryFields}} "{{$field}}": arg{{$index}}, +{{range $index, $field := .Predicates}} {{$field.Code $index}}, {{end}} }) if err != nil { return nil, err diff --git a/internal/spec/models.go b/internal/spec/models.go index 171e3bd..15b4696 100644 --- a/internal/spec/models.go +++ b/internal/spec/models.go @@ -1,6 +1,10 @@ package spec -import "github.com/sunboyy/repogen/internal/code" +import ( + "strings" + + "github.com/sunboyy/repogen/internal/code" +) // QueryMode one or many type QueryMode string @@ -35,12 +39,52 @@ type FindOperation struct { Query QuerySpec } -// QuerySpec is a condition of querying the database +// QuerySpec is a set of conditions of querying the database type QuerySpec struct { - Fields []string + Predicates []Predicate } // NumberOfArguments returns number of arguments required to perform the query func (q QuerySpec) NumberOfArguments() int { - return len(q.Fields) + return len(q.Predicates) +} + +// Operator is an operator of the condition to query the data +type Operator string + +// operator constants +const ( + OperatorEqual Operator = "EQUAL" + OperatorNot Operator = "NOT" + OperatorLessThan Operator = "LESS_THAN" + OperatorLessThanEqual Operator = "LESS_THAN_EQUAL" + OperatorGreaterThan Operator = "GREATER_THAN" + OperatorGreaterThanEqual Operator = "GREATER_THAN_EQUAL" +) + +// Predicate is a criteria for querying a field +type Predicate struct { + Field string + Operator Operator +} + +type predicateToken []string + +func (t predicateToken) ToPredicate() Predicate { + if len(t) > 1 && t[len(t)-1] == "Not" { + return Predicate{Field: strings.Join(t[:len(t)-1], ""), Operator: OperatorNot} + } + if len(t) > 2 && t[len(t)-2] == "Less" && t[len(t)-1] == "Than" { + return Predicate{Field: strings.Join(t[:len(t)-2], ""), Operator: OperatorLessThan} + } + if len(t) > 3 && t[len(t)-3] == "Less" && t[len(t)-2] == "Than" && t[len(t)-1] == "Equal" { + return Predicate{Field: strings.Join(t[:len(t)-3], ""), Operator: OperatorLessThanEqual} + } + if len(t) > 2 && t[len(t)-2] == "Greater" && t[len(t)-1] == "Than" { + return Predicate{Field: strings.Join(t[:len(t)-2], ""), Operator: OperatorGreaterThan} + } + if len(t) > 3 && t[len(t)-3] == "Greater" && t[len(t)-2] == "Than" && t[len(t)-1] == "Equal" { + return Predicate{Field: strings.Join(t[:len(t)-3], ""), Operator: OperatorGreaterThanEqual} + } + return Predicate{Field: strings.Join(t, ""), Operator: OperatorEqual} } diff --git a/internal/spec/parser.go b/internal/spec/parser.go index d08642c..ce6afd0 100644 --- a/internal/spec/parser.go +++ b/internal/spec/parser.go @@ -130,20 +130,21 @@ func (p repositoryInterfaceParser) parseQuery(tokens []string) (QuerySpec, error if tokens[0] == "And" { return QuerySpec{}, errors.New("method name not supported") } - var queryFields []string - var aggregatedToken string + + var predicates []Predicate + var aggregatedToken predicateToken for _, token := range tokens { if token != "And" { - aggregatedToken += token + aggregatedToken = append(aggregatedToken, token) } else { - queryFields = append(queryFields, aggregatedToken) - aggregatedToken = "" + predicates = append(predicates, aggregatedToken.ToPredicate()) + aggregatedToken = predicateToken{} } } - if aggregatedToken == "" { + if len(aggregatedToken) == 0 { return QuerySpec{}, errors.New("method name not supported") } - queryFields = append(queryFields, aggregatedToken) + predicates = append(predicates, aggregatedToken.ToPredicate()) - return QuerySpec{Fields: queryFields}, nil + return QuerySpec{Predicates: predicates}, nil } diff --git a/internal/spec/parser_test.go b/internal/spec/parser_test.go index d5a5f9a..0aeba9a 100644 --- a/internal/spec/parser_test.go +++ b/internal/spec/parser_test.go @@ -57,8 +57,10 @@ func TestParseRepositoryInterface(t *testing.T) { code.SimpleType("error"), }, Operation: spec.FindOperation{ - Mode: spec.QueryModeOne, - Query: spec.QuerySpec{Fields: []string{"ID"}}, + Mode: spec.QueryModeOne, + Query: spec.QuerySpec{Predicates: []spec.Predicate{ + {Field: "ID", Operator: spec.OperatorEqual}, + }}, }, }, }, @@ -96,9 +98,10 @@ func TestParseRepositoryInterface(t *testing.T) { code.SimpleType("error"), }, Operation: spec.FindOperation{ - - Mode: spec.QueryModeOne, - Query: spec.QuerySpec{Fields: []string{"PhoneNumber"}}, + Mode: spec.QueryModeOne, + Query: spec.QuerySpec{Predicates: []spec.Predicate{ + {Field: "PhoneNumber", Operator: spec.OperatorEqual}, + }}, }, }, }, @@ -136,8 +139,10 @@ func TestParseRepositoryInterface(t *testing.T) { code.SimpleType("error"), }, Operation: spec.FindOperation{ - Mode: spec.QueryModeMany, - Query: spec.QuerySpec{Fields: []string{"City"}}, + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{Predicates: []spec.Predicate{ + {Field: "City", Operator: spec.OperatorEqual}, + }}, }, }, }, @@ -213,8 +218,216 @@ func TestParseRepositoryInterface(t *testing.T) { code.SimpleType("error"), }, Operation: spec.FindOperation{ - Mode: spec.QueryModeMany, - Query: spec.QuerySpec{Fields: []string{"City", "Gender"}}, + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{Predicates: []spec.Predicate{ + {Field: "City", Operator: spec.OperatorEqual}, + {Field: "Gender", Operator: spec.OperatorEqual}, + }}, + }, + }, + }, + }, + }, + { + Name: "FindByArgNot method", + Interface: code.Interface{ + Name: "UserRepository", + Methods: []code.Method{ + { + Name: "FindByCityNot", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + }, + Returns: []code.Type{ + code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, + code.SimpleType("error"), + }, + }, + }, + }, + ExpectedOutput: spec.RepositorySpec{ + InterfaceName: "UserRepository", + Methods: []spec.MethodSpec{ + { + Name: "FindByCityNot", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + }, + Returns: []code.Type{ + code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, + code.SimpleType("error"), + }, + Operation: spec.FindOperation{ + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{Predicates: []spec.Predicate{ + {Field: "City", Operator: spec.OperatorNot}, + }}, + }, + }, + }, + }, + }, + { + Name: "FindByArgLessThan method", + Interface: code.Interface{ + Name: "UserRepository", + Methods: []code.Method{ + { + Name: "FindByAgeLessThan", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + }, + Returns: []code.Type{ + code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, + code.SimpleType("error"), + }, + }, + }, + }, + ExpectedOutput: spec.RepositorySpec{ + InterfaceName: "UserRepository", + Methods: []spec.MethodSpec{ + { + Name: "FindByAgeLessThan", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + }, + Returns: []code.Type{ + code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, + code.SimpleType("error"), + }, + Operation: spec.FindOperation{ + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{Predicates: []spec.Predicate{ + {Field: "Age", Operator: spec.OperatorLessThan}, + }}, + }, + }, + }, + }, + }, + { + Name: "FindByArgLessThanEqual method", + Interface: code.Interface{ + Name: "UserRepository", + Methods: []code.Method{ + { + Name: "FindByAgeLessThanEqual", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + }, + Returns: []code.Type{ + code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, + code.SimpleType("error"), + }, + }, + }, + }, + ExpectedOutput: spec.RepositorySpec{ + InterfaceName: "UserRepository", + Methods: []spec.MethodSpec{ + { + Name: "FindByAgeLessThanEqual", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + }, + Returns: []code.Type{ + code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, + code.SimpleType("error"), + }, + Operation: spec.FindOperation{ + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{Predicates: []spec.Predicate{ + {Field: "Age", Operator: spec.OperatorLessThanEqual}, + }}, + }, + }, + }, + }, + }, + { + Name: "FindByArgGreaterThan method", + Interface: code.Interface{ + Name: "UserRepository", + Methods: []code.Method{ + { + Name: "FindByAgeGreaterThan", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + }, + Returns: []code.Type{ + code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, + code.SimpleType("error"), + }, + }, + }, + }, + ExpectedOutput: spec.RepositorySpec{ + InterfaceName: "UserRepository", + Methods: []spec.MethodSpec{ + { + Name: "FindByAgeGreaterThan", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + }, + Returns: []code.Type{ + code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, + code.SimpleType("error"), + }, + Operation: spec.FindOperation{ + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{Predicates: []spec.Predicate{ + {Field: "Age", Operator: spec.OperatorGreaterThan}, + }}, + }, + }, + }, + }, + }, + { + Name: "FindByArgGreaterThanEqual method", + Interface: code.Interface{ + Name: "UserRepository", + Methods: []code.Method{ + { + Name: "FindByAgeGreaterThanEqual", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + }, + Returns: []code.Type{ + code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, + code.SimpleType("error"), + }, + }, + }, + }, + ExpectedOutput: spec.RepositorySpec{ + InterfaceName: "UserRepository", + Methods: []spec.MethodSpec{ + { + Name: "FindByAgeGreaterThanEqual", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + }, + Returns: []code.Type{ + code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, + code.SimpleType("error"), + }, + Operation: spec.FindOperation{ + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{Predicates: []spec.Predicate{ + {Field: "Age", Operator: spec.OperatorGreaterThanEqual}, + }}, }, }, }, @@ -241,6 +454,10 @@ func TestParseRepositoryInterface(t *testing.T) { Name: "City", Type: code.SimpleType("string"), }, + { + Name: "Age", + Type: code.SimpleType("int"), + }, }, }