Add simple comparison predicates

This commit is contained in:
sunboyy 2021-01-19 19:26:26 +07:00
parent 5e7d773e1b
commit bc878c778c
9 changed files with 465 additions and 34 deletions

View file

@ -7,7 +7,7 @@ on:
branches: [ main ] branches: [ main ]
jobs: jobs:
build: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View file

@ -5,3 +5,7 @@
</a> </a>
Repogen is a code generator for database repository in Golang. (WIP) 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.

View file

@ -120,24 +120,24 @@ func (g mongoRepositoryGenerator) generateMethodImplementation(methodSpec spec.M
func (g mongoRepositoryGenerator) generateFindImplementation(operation spec.FindOperation) (string, error) { func (g mongoRepositoryGenerator) generateFindImplementation(operation spec.FindOperation) (string, error) {
buffer := new(bytes.Buffer) buffer := new(bytes.Buffer)
var queryFields []string var predicates []predicate
for _, fieldName := range operation.Query.Fields { for _, predicateSpec := range operation.Query.Predicates {
structField, ok := g.StructModel.Fields.ByName(fieldName) structField, ok := g.StructModel.Fields.ByName(predicateSpec.Field)
if !ok { 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"] bsonTag, ok := structField.Tags["bson"]
if !ok { 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{ tmplData := mongoFindTemplateData{
EntityType: g.StructModel.Name, EntityType: g.StructModel.Name,
QueryFields: queryFields, Predicates: predicates,
} }
if operation.Mode == spec.QueryModeOne { if operation.Mode == spec.QueryModeOne {

View file

@ -22,6 +22,16 @@ func TestGenerateMongoRepository(t *testing.T) {
Type: code.SimpleType("string"), Type: code.SimpleType("string"),
Tags: map[string][]string{"bson": {"username"}}, 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{ intf := code.Interface{
@ -69,6 +79,61 @@ func TestGenerateMongoRepository(t *testing.T) {
code.SimpleType("error"), 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 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") expectedCodeLines := strings.Split(expectedCode, "\n")
actualCodeLines := strings.Split(code, "\n") actualCodeLines := strings.Split(code, "\n")

30
internal/mongo/models.go Normal file
View file

@ -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 ""
}

View file

@ -75,19 +75,19 @@ func (data mongoMethodTemplateData) Returns() string {
const findOneTemplate = ` var entity {{.EntityType}} const findOneTemplate = ` var entity {{.EntityType}}
if err := r.collection.FindOne(ctx, bson.M{ 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 { {{end}} }).Decode(&entity); err != nil {
return nil, err return nil, err
} }
return &entity, nil` return &entity, nil`
type mongoFindTemplateData struct { type mongoFindTemplateData struct {
EntityType string EntityType string
QueryFields []string Predicates []predicate
} }
const findManyTemplate = ` cursor, err := r.collection.Find(ctx, bson.M{ 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}} }) {{end}} })
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -1,6 +1,10 @@
package spec package spec
import "github.com/sunboyy/repogen/internal/code" import (
"strings"
"github.com/sunboyy/repogen/internal/code"
)
// QueryMode one or many // QueryMode one or many
type QueryMode string type QueryMode string
@ -35,12 +39,52 @@ type FindOperation struct {
Query QuerySpec Query QuerySpec
} }
// QuerySpec is a condition of querying the database // QuerySpec is a set of conditions of querying the database
type QuerySpec struct { type QuerySpec struct {
Fields []string Predicates []Predicate
} }
// NumberOfArguments returns number of arguments required to perform the query // NumberOfArguments returns number of arguments required to perform the query
func (q QuerySpec) NumberOfArguments() int { 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}
} }

View file

@ -130,20 +130,21 @@ func (p repositoryInterfaceParser) parseQuery(tokens []string) (QuerySpec, error
if tokens[0] == "And" { if tokens[0] == "And" {
return QuerySpec{}, errors.New("method name not supported") return QuerySpec{}, errors.New("method name not supported")
} }
var queryFields []string
var aggregatedToken string var predicates []Predicate
var aggregatedToken predicateToken
for _, token := range tokens { for _, token := range tokens {
if token != "And" { if token != "And" {
aggregatedToken += token aggregatedToken = append(aggregatedToken, token)
} else { } else {
queryFields = append(queryFields, aggregatedToken) predicates = append(predicates, aggregatedToken.ToPredicate())
aggregatedToken = "" aggregatedToken = predicateToken{}
} }
} }
if aggregatedToken == "" { if len(aggregatedToken) == 0 {
return QuerySpec{}, errors.New("method name not supported") 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
} }

View file

@ -57,8 +57,10 @@ func TestParseRepositoryInterface(t *testing.T) {
code.SimpleType("error"), code.SimpleType("error"),
}, },
Operation: spec.FindOperation{ Operation: spec.FindOperation{
Mode: spec.QueryModeOne, Mode: spec.QueryModeOne,
Query: spec.QuerySpec{Fields: []string{"ID"}}, Query: spec.QuerySpec{Predicates: []spec.Predicate{
{Field: "ID", Operator: spec.OperatorEqual},
}},
}, },
}, },
}, },
@ -96,9 +98,10 @@ func TestParseRepositoryInterface(t *testing.T) {
code.SimpleType("error"), code.SimpleType("error"),
}, },
Operation: spec.FindOperation{ Operation: spec.FindOperation{
Mode: spec.QueryModeOne,
Mode: spec.QueryModeOne, Query: spec.QuerySpec{Predicates: []spec.Predicate{
Query: spec.QuerySpec{Fields: []string{"PhoneNumber"}}, {Field: "PhoneNumber", Operator: spec.OperatorEqual},
}},
}, },
}, },
}, },
@ -136,8 +139,10 @@ func TestParseRepositoryInterface(t *testing.T) {
code.SimpleType("error"), code.SimpleType("error"),
}, },
Operation: spec.FindOperation{ Operation: spec.FindOperation{
Mode: spec.QueryModeMany, Mode: spec.QueryModeMany,
Query: spec.QuerySpec{Fields: []string{"City"}}, Query: spec.QuerySpec{Predicates: []spec.Predicate{
{Field: "City", Operator: spec.OperatorEqual},
}},
}, },
}, },
}, },
@ -213,8 +218,216 @@ func TestParseRepositoryInterface(t *testing.T) {
code.SimpleType("error"), code.SimpleType("error"),
}, },
Operation: spec.FindOperation{ Operation: spec.FindOperation{
Mode: spec.QueryModeMany, Mode: spec.QueryModeMany,
Query: spec.QuerySpec{Fields: []string{"City", "Gender"}}, 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", Name: "City",
Type: code.SimpleType("string"), Type: code.SimpleType("string"),
}, },
{
Name: "Age",
Type: code.SimpleType("int"),
},
}, },
} }