Merge pull request #6 from sunboyy/method-signature-validation

Validate parameter types in method signature
This commit is contained in:
sunboyy 2021-01-21 18:58:05 +07:00 committed by GitHub
commit 847d5add53
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 587 additions and 488 deletions

View file

@ -2,7 +2,7 @@ coverage:
status: status:
project: project:
default: default:
target: 65% target: 70%
threshold: 5% threshold: 5%
patch: patch:
default: default:

View file

@ -13,15 +13,20 @@ import (
// GenerateMongoRepository generates mongodb repository // GenerateMongoRepository generates mongodb repository
func GenerateMongoRepository(packageName string, structModel code.Struct, intf code.Interface) (string, error) { func GenerateMongoRepository(packageName string, structModel code.Struct, intf code.Interface) (string, error) {
repositorySpec, err := spec.ParseRepositoryInterface(structModel, intf) var methodSpecs []spec.MethodSpec
if err != nil { for _, method := range intf.Methods {
return "", err methodSpec, err := spec.ParseInterfaceMethod(structModel, method)
if err != nil {
return "", err
}
methodSpecs = append(methodSpecs, methodSpec)
} }
generator := mongoRepositoryGenerator{ generator := mongoRepositoryGenerator{
PackageName: packageName, PackageName: packageName,
StructModel: structModel, StructModel: structModel,
RepositorySpec: repositorySpec, InterfaceName: intf.Name,
MethodSpecs: methodSpecs,
} }
output, err := generator.Generate() output, err := generator.Generate()
@ -33,9 +38,10 @@ func GenerateMongoRepository(packageName string, structModel code.Struct, intf c
} }
type mongoRepositoryGenerator struct { type mongoRepositoryGenerator struct {
PackageName string PackageName string
StructModel code.Struct StructModel code.Struct
RepositorySpec spec.RepositorySpec InterfaceName string
MethodSpecs []spec.MethodSpec
} }
func (g mongoRepositoryGenerator) Generate() (string, error) { func (g mongoRepositoryGenerator) Generate() (string, error) {
@ -44,7 +50,7 @@ func (g mongoRepositoryGenerator) Generate() (string, error) {
return "", err return "", err
} }
for _, method := range g.RepositorySpec.Methods { for _, method := range g.MethodSpecs {
if err := g.generateMethod(buffer, method); err != nil { if err := g.generateMethod(buffer, method); err != nil {
return "", err return "", err
} }
@ -66,7 +72,7 @@ func (g mongoRepositoryGenerator) generateBaseContent(buffer *bytes.Buffer) erro
tmplData := mongoBaseTemplateData{ tmplData := mongoBaseTemplateData{
PackageName: g.PackageName, PackageName: g.PackageName,
InterfaceName: g.RepositorySpec.InterfaceName, InterfaceName: g.InterfaceName,
StructName: g.structName(), StructName: g.structName(),
} }
@ -167,5 +173,5 @@ func (g mongoRepositoryGenerator) generateFindImplementation(operation spec.Find
} }
func (g mongoRepositoryGenerator) structName() string { func (g mongoRepositoryGenerator) structName() string {
return g.RepositorySpec.InterfaceName + "Mongo" return g.InterfaceName + "Mongo"
} }

View file

@ -83,7 +83,7 @@ func TestGenerateMongoRepository(t *testing.T) {
Name: "FindByGenderNot", Name: "FindByGenderNot",
Params: []code.Param{ Params: []code.Param{
{Name: "ctx", Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, {Name: "ctx", Type: code.ExternalType{PackageAlias: "context", Name: "Context"}},
{Name: "gender", Type: code.SimpleType("int")}, {Name: "gender", Type: code.SimpleType("Gender")},
}, },
Returns: []code.Type{ Returns: []code.Type{
code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
@ -220,7 +220,7 @@ 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) { func (r *UserRepositoryMongo) FindByGenderNot(ctx context.Context, arg0 Gender) ([]*UserModel, error) {
cursor, err := r.collection.Find(ctx, bson.M{ cursor, err := r.collection.Find(ctx, bson.M{
"gender": bson.M{"$ne": arg0}, "gender": bson.M{"$ne": arg0},
}) })

35
internal/spec/errors.go Normal file
View file

@ -0,0 +1,35 @@
package spec
// ParsingError is an error from parsing interface methods
type ParsingError string
func (err ParsingError) Error() string {
switch err {
case UnknownOperationError:
return "unknown operation"
case UnsupportedNameError:
return "method name is not supported"
case InvalidQueryError:
return "invalid query"
case InvalidParamError:
return "parameters do not match the query"
case UnsupportedReturnError:
return "this type of return is not supported"
case ContextParamRequiredError:
return "context parameter is required"
case StructFieldNotFoundError:
return "struct field not found"
}
return string(err)
}
// parsing error constants
const (
UnknownOperationError ParsingError = "ERROR_UNKNOWN_OPERATION"
UnsupportedNameError ParsingError = "ERROR_UNSUPPORTED"
InvalidQueryError ParsingError = "ERROR_INVALID_QUERY"
InvalidParamError ParsingError = "ERROR_INVALID_PARAM"
UnsupportedReturnError ParsingError = "ERROR_INVALID_RETURN"
ContextParamRequiredError ParsingError = "ERROR_CONTEXT_PARAM_REQUIRED"
StructFieldNotFoundError ParsingError = "ERROR_STRUCT_FIELD_NOT_FOUND"
)

View file

@ -15,12 +15,6 @@ const (
QueryModeMany QueryMode = "MANY" QueryModeMany QueryMode = "MANY"
) )
// RepositorySpec is a specification generated from the repository interface
type RepositorySpec struct {
InterfaceName string
Methods []MethodSpec
}
// MethodSpec is a method specification inside repository specification // MethodSpec is a method specification inside repository specification
type MethodSpec struct { type MethodSpec struct {
Name string Name string

View file

@ -1,59 +1,40 @@
package spec package spec
import ( import (
"errors"
"fmt"
"github.com/fatih/camelcase" "github.com/fatih/camelcase"
"github.com/sunboyy/repogen/internal/code" "github.com/sunboyy/repogen/internal/code"
) )
// ParseRepositoryInterface returns repository spec from declared repository interface // ParseInterfaceMethod returns repository method spec from declared interface method
func ParseRepositoryInterface(structModel code.Struct, intf code.Interface) (RepositorySpec, error) { func ParseInterfaceMethod(structModel code.Struct, method code.Method) (MethodSpec, error) {
parser := repositoryInterfaceParser{ parser := interfaceMethodParser{
StructModel: structModel, StructModel: structModel,
Interface: intf, Method: method,
} }
return parser.Parse() return parser.Parse()
} }
type repositoryInterfaceParser struct { type interfaceMethodParser struct {
StructModel code.Struct StructModel code.Struct
Interface code.Interface Method code.Method
} }
func (p repositoryInterfaceParser) Parse() (RepositorySpec, error) { func (p interfaceMethodParser) Parse() (MethodSpec, error) {
repositorySpec := RepositorySpec{ methodNameTokens := camelcase.Split(p.Method.Name)
InterfaceName: p.Interface.Name,
}
for _, method := range p.Interface.Methods {
methodSpec, err := p.parseMethod(method)
if err != nil {
return RepositorySpec{}, err
}
repositorySpec.Methods = append(repositorySpec.Methods, methodSpec)
}
return repositorySpec, nil
}
func (p repositoryInterfaceParser) parseMethod(method code.Method) (MethodSpec, error) {
methodNameTokens := camelcase.Split(method.Name)
switch methodNameTokens[0] { switch methodNameTokens[0] {
case "Find": case "Find":
return p.parseFindMethod(method, methodNameTokens[1:]) return p.parseFindMethod(methodNameTokens[1:])
} }
return MethodSpec{}, errors.New("method name not supported") return MethodSpec{}, UnknownOperationError
} }
func (p repositoryInterfaceParser) parseFindMethod(method code.Method, tokens []string) (MethodSpec, error) { func (p interfaceMethodParser) parseFindMethod(tokens []string) (MethodSpec, error) {
if len(tokens) == 0 { if len(tokens) == 0 {
return MethodSpec{}, errors.New("method name not supported") return MethodSpec{}, UnsupportedNameError
} }
mode, err := p.extractFindReturns(method.Returns) mode, err := p.extractFindReturns(p.Method.Returns)
if err != nil { if err != nil {
return MethodSpec{}, err return MethodSpec{}, err
} }
@ -63,14 +44,14 @@ func (p repositoryInterfaceParser) parseFindMethod(method code.Method, tokens []
return MethodSpec{}, err return MethodSpec{}, err
} }
if querySpec.NumberOfArguments()+1 != len(method.Params) { if err := p.validateMethodSignature(querySpec); err != nil {
return MethodSpec{}, errors.New("method parameter not supported") return MethodSpec{}, err
} }
return MethodSpec{ return MethodSpec{
Name: method.Name, Name: p.Method.Name,
Params: method.Params, Params: p.Method.Params,
Returns: method.Returns, Returns: p.Method.Returns,
Operation: FindOperation{ Operation: FindOperation{
Mode: mode, Mode: mode,
Query: querySpec, Query: querySpec,
@ -78,13 +59,13 @@ func (p repositoryInterfaceParser) parseFindMethod(method code.Method, tokens []
}, nil }, nil
} }
func (p repositoryInterfaceParser) extractFindReturns(returns []code.Type) (QueryMode, error) { func (p interfaceMethodParser) extractFindReturns(returns []code.Type) (QueryMode, error) {
if len(returns) != 2 { if len(returns) != 2 {
return "", errors.New("method return not supported") return "", UnsupportedReturnError
} }
if returns[1] != code.SimpleType("error") { if returns[1] != code.SimpleType("error") {
return "", errors.New("method return not supported") return "", UnsupportedReturnError
} }
pointerType, ok := returns[0].(code.PointerType) pointerType, ok := returns[0].(code.PointerType)
@ -93,7 +74,7 @@ func (p repositoryInterfaceParser) extractFindReturns(returns []code.Type) (Quer
if simpleType == code.SimpleType(p.StructModel.Name) { if simpleType == code.SimpleType(p.StructModel.Name) {
return QueryModeOne, nil return QueryModeOne, nil
} }
return "", fmt.Errorf("invalid return type %s", pointerType.Code()) return "", UnsupportedReturnError
} }
arrayType, ok := returns[0].(code.ArrayType) arrayType, ok := returns[0].(code.ArrayType)
@ -104,16 +85,16 @@ func (p repositoryInterfaceParser) extractFindReturns(returns []code.Type) (Quer
if simpleType == code.SimpleType(p.StructModel.Name) { if simpleType == code.SimpleType(p.StructModel.Name) {
return QueryModeMany, nil return QueryModeMany, nil
} }
return "", fmt.Errorf("invalid return type %s", pointerType.Code()) return "", UnsupportedReturnError
} }
} }
return "", errors.New("method return not supported") return "", UnsupportedReturnError
} }
func (p repositoryInterfaceParser) parseQuery(tokens []string) (QuerySpec, error) { func (p interfaceMethodParser) parseQuery(tokens []string) (QuerySpec, error) {
if len(tokens) == 0 { if len(tokens) == 0 {
return QuerySpec{}, errors.New("method name not supported") return QuerySpec{}, InvalidQueryError
} }
if len(tokens) == 1 && tokens[0] == "All" { if len(tokens) == 1 && tokens[0] == "All" {
@ -128,7 +109,7 @@ func (p repositoryInterfaceParser) parseQuery(tokens []string) (QuerySpec, error
} }
if tokens[0] == "And" || tokens[0] == "Or" { if tokens[0] == "And" || tokens[0] == "Or" {
return QuerySpec{}, errors.New("method name not supported") return QuerySpec{}, InvalidQueryError
} }
var operator Operator var operator Operator
@ -137,6 +118,8 @@ func (p repositoryInterfaceParser) parseQuery(tokens []string) (QuerySpec, error
for _, token := range tokens { for _, token := range tokens {
if token != "And" && token != "Or" { if token != "And" && token != "Or" {
aggregatedToken = append(aggregatedToken, token) aggregatedToken = append(aggregatedToken, token)
} else if len(aggregatedToken) == 0 {
return QuerySpec{}, InvalidQueryError
} else if token == "And" && operator != OperatorOr { } else if token == "And" && operator != OperatorOr {
operator = OperatorAnd operator = OperatorAnd
predicates = append(predicates, aggregatedToken.ToPredicate()) predicates = append(predicates, aggregatedToken.ToPredicate())
@ -146,13 +129,40 @@ func (p repositoryInterfaceParser) parseQuery(tokens []string) (QuerySpec, error
predicates = append(predicates, aggregatedToken.ToPredicate()) predicates = append(predicates, aggregatedToken.ToPredicate())
aggregatedToken = predicateToken{} aggregatedToken = predicateToken{}
} else { } else {
return QuerySpec{}, errors.New("method name contains ambiguous query") return QuerySpec{}, InvalidQueryError
} }
} }
if len(aggregatedToken) == 0 { if len(aggregatedToken) == 0 {
return QuerySpec{}, errors.New("method name not supported") return QuerySpec{}, InvalidQueryError
} }
predicates = append(predicates, aggregatedToken.ToPredicate()) predicates = append(predicates, aggregatedToken.ToPredicate())
return QuerySpec{Operator: operator, Predicates: predicates}, nil return QuerySpec{Operator: operator, Predicates: predicates}, nil
} }
func (p interfaceMethodParser) validateMethodSignature(querySpec QuerySpec) error {
contextType := code.ExternalType{PackageAlias: "context", Name: "Context"}
if len(p.Method.Params) == 0 || p.Method.Params[0].Type != contextType {
return ContextParamRequiredError
}
if querySpec.NumberOfArguments()+1 != len(p.Method.Params) {
return InvalidParamError
}
currentParamIndex := 1
for _, predicate := range querySpec.Predicates {
structField, ok := p.StructModel.Fields.ByName(predicate.Field)
if !ok {
return StructFieldNotFoundError
}
if structField.Type != p.Method.Params[currentParamIndex].Type {
return InvalidParamError
}
currentParamIndex++
}
return nil
}

View file

@ -8,224 +8,191 @@ import (
"github.com/sunboyy/repogen/internal/spec" "github.com/sunboyy/repogen/internal/spec"
) )
type TestCase struct { var structModel = code.Struct{
Name string Name: "UserModel",
Interface code.Interface Fields: code.StructFields{
ExpectedOutput spec.RepositorySpec
}
func TestParseRepositoryInterface(t *testing.T) {
testTable := []TestCase{
{ {
Name: "interface spec", Name: "ID",
Interface: code.Interface{ Type: code.ExternalType{PackageAlias: "primitive", Name: "ObjectID"},
Name: "UserRepository",
},
ExpectedOutput: spec.RepositorySpec{
InterfaceName: "UserRepository",
},
}, },
{
Name: "PhoneNumber",
Type: code.SimpleType("string"),
},
{
Name: "Gender",
Type: code.SimpleType("Gender"),
},
{
Name: "City",
Type: code.SimpleType("string"),
},
{
Name: "Age",
Type: code.SimpleType("int"),
},
},
}
type ParseInterfaceMethodTestCase struct {
Name string
Method code.Method
ExpectedOutput spec.MethodSpec
}
func TestParseInterfaceMethod(t *testing.T) {
testTable := []ParseInterfaceMethodTestCase{
{ {
Name: "FindOneByArg method", Name: "FindOneByArg method",
Interface: code.Interface{ Method: code.Method{
Name: "UserRepository", Name: "FindOneByID",
Methods: []code.Method{ Params: []code.Param{
{ {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}},
Name: "FindOneByID", {Type: code.ExternalType{PackageAlias: "primitive", Name: "ObjectID"}},
Params: []code.Param{ },
{Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, Returns: []code.Type{
{Type: code.ExternalType{PackageAlias: "primitive", Name: "ObjectID"}}, code.PointerType{ContainedType: code.SimpleType("UserModel")},
}, code.SimpleType("error"),
Returns: []code.Type{
code.PointerType{ContainedType: code.SimpleType("UserModel")},
code.SimpleType("error"),
},
},
}, },
}, },
ExpectedOutput: spec.RepositorySpec{ ExpectedOutput: spec.MethodSpec{
InterfaceName: "UserRepository", Name: "FindOneByID",
Methods: []spec.MethodSpec{ Params: []code.Param{
{ {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}},
Name: "FindOneByID", {Type: code.ExternalType{PackageAlias: "primitive", Name: "ObjectID"}},
Params: []code.Param{ },
{Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, Returns: []code.Type{
{Type: code.ExternalType{PackageAlias: "primitive", Name: "ObjectID"}}, code.PointerType{ContainedType: code.SimpleType("UserModel")},
}, code.SimpleType("error"),
Returns: []code.Type{ },
code.PointerType{ContainedType: code.SimpleType("UserModel")}, Operation: spec.FindOperation{
code.SimpleType("error"), Mode: spec.QueryModeOne,
}, Query: spec.QuerySpec{Predicates: []spec.Predicate{
Operation: spec.FindOperation{ {Field: "ID", Comparator: spec.ComparatorEqual},
Mode: spec.QueryModeOne, }},
Query: spec.QuerySpec{Predicates: []spec.Predicate{
{Field: "ID", Comparator: spec.ComparatorEqual},
}},
},
},
}, },
}, },
}, },
{ {
Name: "FindOneByMultiWordArg method", Name: "FindOneByMultiWordArg method",
Interface: code.Interface{ Method: code.Method{
Name: "UserRepository", Name: "FindOneByPhoneNumber",
Methods: []code.Method{ Params: []code.Param{
{ {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}},
Name: "FindOneByPhoneNumber", {Type: code.SimpleType("string")},
Params: []code.Param{ },
{Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, Returns: []code.Type{
{Type: code.SimpleType("string")}, code.PointerType{ContainedType: code.SimpleType("UserModel")},
}, code.SimpleType("error"),
Returns: []code.Type{
code.PointerType{ContainedType: code.SimpleType("UserModel")},
code.SimpleType("error"),
},
},
}, },
}, },
ExpectedOutput: spec.RepositorySpec{ ExpectedOutput: spec.MethodSpec{
InterfaceName: "UserRepository", Name: "FindOneByPhoneNumber",
Methods: []spec.MethodSpec{ Params: []code.Param{
{ {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}},
Name: "FindOneByPhoneNumber", {Type: code.SimpleType("string")},
Params: []code.Param{ },
{Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, Returns: []code.Type{
{Type: code.SimpleType("string")}, code.PointerType{ContainedType: code.SimpleType("UserModel")},
}, code.SimpleType("error"),
Returns: []code.Type{ },
code.PointerType{ContainedType: code.SimpleType("UserModel")}, Operation: spec.FindOperation{
code.SimpleType("error"), Mode: spec.QueryModeOne,
}, Query: spec.QuerySpec{Predicates: []spec.Predicate{
Operation: spec.FindOperation{ {Field: "PhoneNumber", Comparator: spec.ComparatorEqual},
Mode: spec.QueryModeOne, }},
Query: spec.QuerySpec{Predicates: []spec.Predicate{
{Field: "PhoneNumber", Comparator: spec.ComparatorEqual},
}},
},
},
}, },
}, },
}, },
{ {
Name: "FindByArg method", Name: "FindByArg method",
Interface: code.Interface{ Method: code.Method{
Name: "UserRepository", Name: "FindByCity",
Methods: []code.Method{ Params: []code.Param{
{ {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}},
Name: "FindByCity", {Type: code.SimpleType("string")},
Params: []code.Param{ },
{Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, Returns: []code.Type{
{Type: code.SimpleType("string")}, code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
}, code.SimpleType("error"),
Returns: []code.Type{
code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
code.SimpleType("error"),
},
},
}, },
}, },
ExpectedOutput: spec.RepositorySpec{ ExpectedOutput: spec.MethodSpec{
InterfaceName: "UserRepository", Name: "FindByCity",
Methods: []spec.MethodSpec{ Params: []code.Param{
{ {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}},
Name: "FindByCity", {Type: code.SimpleType("string")},
Params: []code.Param{ },
{Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, Returns: []code.Type{
{Type: code.SimpleType("string")}, code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
}, code.SimpleType("error"),
Returns: []code.Type{ },
code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, Operation: spec.FindOperation{
code.SimpleType("error"), Mode: spec.QueryModeMany,
}, Query: spec.QuerySpec{Predicates: []spec.Predicate{
Operation: spec.FindOperation{ {Field: "City", Comparator: spec.ComparatorEqual},
Mode: spec.QueryModeMany, }},
Query: spec.QuerySpec{Predicates: []spec.Predicate{
{Field: "City", Comparator: spec.ComparatorEqual},
}},
},
},
}, },
}, },
}, },
{ {
Name: "FindAll method", Name: "FindAll method",
Interface: code.Interface{ Method: code.Method{
Name: "UserRepository", Name: "FindAll",
Methods: []code.Method{ Params: []code.Param{
{ {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}},
Name: "FindAll", },
Params: []code.Param{ Returns: []code.Type{
{Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
}, code.SimpleType("error"),
Returns: []code.Type{
code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
code.SimpleType("error"),
},
},
}, },
}, },
ExpectedOutput: spec.RepositorySpec{ ExpectedOutput: spec.MethodSpec{
InterfaceName: "UserRepository", Name: "FindAll",
Methods: []spec.MethodSpec{ Params: []code.Param{
{ {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}},
Name: "FindAll", },
Params: []code.Param{ Returns: []code.Type{
{Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
}, code.SimpleType("error"),
Returns: []code.Type{ },
code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, Operation: spec.FindOperation{
code.SimpleType("error"), Mode: spec.QueryModeMany,
},
Operation: spec.FindOperation{
Mode: spec.QueryModeMany,
},
},
}, },
}, },
}, },
{ {
Name: "FindByArgAndArg method", Name: "FindByArgAndArg method",
Interface: code.Interface{ Method: code.Method{
Name: "UserRepository", Name: "FindByCityAndGender",
Methods: []code.Method{ Params: []code.Param{
{ {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}},
Name: "FindByCityAndGender", {Type: code.SimpleType("string")},
Params: []code.Param{ {Type: code.SimpleType("Gender")},
{Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, },
{Type: code.SimpleType("string")}, Returns: []code.Type{
{Type: code.SimpleType("Gender")}, code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
}, code.SimpleType("error"),
Returns: []code.Type{
code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
code.SimpleType("error"),
},
},
}, },
}, },
ExpectedOutput: spec.RepositorySpec{ ExpectedOutput: spec.MethodSpec{
InterfaceName: "UserRepository", Name: "FindByCityAndGender",
Methods: []spec.MethodSpec{ Params: []code.Param{
{ {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}},
Name: "FindByCityAndGender", {Type: code.SimpleType("string")},
Params: []code.Param{ {Type: code.SimpleType("Gender")},
{Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, },
{Type: code.SimpleType("string")}, Returns: []code.Type{
{Type: code.SimpleType("Gender")}, code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
}, code.SimpleType("error"),
Returns: []code.Type{ },
code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, Operation: spec.FindOperation{
code.SimpleType("error"), Mode: spec.QueryModeMany,
}, Query: spec.QuerySpec{
Operation: spec.FindOperation{ Operator: spec.OperatorAnd,
Mode: spec.QueryModeMany, Predicates: []spec.Predicate{
Query: spec.QuerySpec{ {Field: "City", Comparator: spec.ComparatorEqual},
Operator: spec.OperatorAnd, {Field: "Gender", Comparator: spec.ComparatorEqual},
Predicates: []spec.Predicate{
{Field: "City", Comparator: spec.ComparatorEqual},
{Field: "Gender", Comparator: spec.ComparatorEqual},
},
},
}, },
}, },
}, },
@ -233,46 +200,36 @@ func TestParseRepositoryInterface(t *testing.T) {
}, },
{ {
Name: "FindByArgOrArg method", Name: "FindByArgOrArg method",
Interface: code.Interface{ Method: code.Method{
Name: "UserRepository", Name: "FindByCityOrGender",
Methods: []code.Method{ Params: []code.Param{
{ {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}},
Name: "FindByCityOrGender", {Type: code.SimpleType("string")},
Params: []code.Param{ {Type: code.SimpleType("Gender")},
{Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, },
{Type: code.SimpleType("string")}, Returns: []code.Type{
{Type: code.SimpleType("Gender")}, code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
}, code.SimpleType("error"),
Returns: []code.Type{
code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
code.SimpleType("error"),
},
},
}, },
}, },
ExpectedOutput: spec.RepositorySpec{ ExpectedOutput: spec.MethodSpec{
InterfaceName: "UserRepository", Name: "FindByCityOrGender",
Methods: []spec.MethodSpec{ Params: []code.Param{
{ {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}},
Name: "FindByCityOrGender", {Type: code.SimpleType("string")},
Params: []code.Param{ {Type: code.SimpleType("Gender")},
{Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, },
{Type: code.SimpleType("string")}, Returns: []code.Type{
{Type: code.SimpleType("Gender")}, code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
}, code.SimpleType("error"),
Returns: []code.Type{ },
code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, Operation: spec.FindOperation{
code.SimpleType("error"), Mode: spec.QueryModeMany,
}, Query: spec.QuerySpec{
Operation: spec.FindOperation{ Operator: spec.OperatorOr,
Mode: spec.QueryModeMany, Predicates: []spec.Predicate{
Query: spec.QuerySpec{ {Field: "City", Comparator: spec.ComparatorEqual},
Operator: spec.OperatorOr, {Field: "Gender", Comparator: spec.ComparatorEqual},
Predicates: []spec.Predicate{
{Field: "City", Comparator: spec.ComparatorEqual},
{Field: "Gender", Comparator: spec.ComparatorEqual},
},
},
}, },
}, },
}, },
@ -280,240 +237,164 @@ func TestParseRepositoryInterface(t *testing.T) {
}, },
{ {
Name: "FindByArgNot method", Name: "FindByArgNot method",
Interface: code.Interface{ Method: code.Method{
Name: "UserRepository", Name: "FindByCityNot",
Methods: []code.Method{ Params: []code.Param{
{ {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}},
Name: "FindByCityNot", {Type: code.SimpleType("string")},
Params: []code.Param{ },
{Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, Returns: []code.Type{
{Type: code.SimpleType("string")}, code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
}, code.SimpleType("error"),
Returns: []code.Type{
code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
code.SimpleType("error"),
},
},
}, },
}, },
ExpectedOutput: spec.RepositorySpec{ ExpectedOutput: spec.MethodSpec{
InterfaceName: "UserRepository", Name: "FindByCityNot",
Methods: []spec.MethodSpec{ Params: []code.Param{
{ {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}},
Name: "FindByCityNot", {Type: code.SimpleType("string")},
Params: []code.Param{ },
{Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, Returns: []code.Type{
{Type: code.SimpleType("string")}, code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
}, code.SimpleType("error"),
Returns: []code.Type{ },
code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, Operation: spec.FindOperation{
code.SimpleType("error"), Mode: spec.QueryModeMany,
}, Query: spec.QuerySpec{Predicates: []spec.Predicate{
Operation: spec.FindOperation{ {Field: "City", Comparator: spec.ComparatorNot},
Mode: spec.QueryModeMany, }},
Query: spec.QuerySpec{Predicates: []spec.Predicate{
{Field: "City", Comparator: spec.ComparatorNot},
}},
},
},
}, },
}, },
}, },
{ {
Name: "FindByArgLessThan method", Name: "FindByArgLessThan method",
Interface: code.Interface{ Method: code.Method{
Name: "UserRepository", Name: "FindByAgeLessThan",
Methods: []code.Method{ Params: []code.Param{
{ {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}},
Name: "FindByAgeLessThan", {Type: code.SimpleType("int")},
Params: []code.Param{ },
{Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, Returns: []code.Type{
{Type: code.SimpleType("string")}, code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
}, code.SimpleType("error"),
Returns: []code.Type{
code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
code.SimpleType("error"),
},
},
}, },
}, },
ExpectedOutput: spec.RepositorySpec{ ExpectedOutput: spec.MethodSpec{
InterfaceName: "UserRepository", Name: "FindByAgeLessThan",
Methods: []spec.MethodSpec{ Params: []code.Param{
{ {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}},
Name: "FindByAgeLessThan", {Type: code.SimpleType("int")},
Params: []code.Param{ },
{Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, Returns: []code.Type{
{Type: code.SimpleType("string")}, code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
}, code.SimpleType("error"),
Returns: []code.Type{ },
code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, Operation: spec.FindOperation{
code.SimpleType("error"), Mode: spec.QueryModeMany,
}, Query: spec.QuerySpec{Predicates: []spec.Predicate{
Operation: spec.FindOperation{ {Field: "Age", Comparator: spec.ComparatorLessThan},
Mode: spec.QueryModeMany, }},
Query: spec.QuerySpec{Predicates: []spec.Predicate{
{Field: "Age", Comparator: spec.ComparatorLessThan},
}},
},
},
}, },
}, },
}, },
{ {
Name: "FindByArgLessThanEqual method", Name: "FindByArgLessThanEqual method",
Interface: code.Interface{ Method: code.Method{
Name: "UserRepository", Name: "FindByAgeLessThanEqual",
Methods: []code.Method{ Params: []code.Param{
{ {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}},
Name: "FindByAgeLessThanEqual", {Type: code.SimpleType("int")},
Params: []code.Param{ },
{Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, Returns: []code.Type{
{Type: code.SimpleType("string")}, code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
}, code.SimpleType("error"),
Returns: []code.Type{
code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
code.SimpleType("error"),
},
},
}, },
}, },
ExpectedOutput: spec.RepositorySpec{ ExpectedOutput: spec.MethodSpec{
InterfaceName: "UserRepository", Name: "FindByAgeLessThanEqual",
Methods: []spec.MethodSpec{ Params: []code.Param{
{ {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}},
Name: "FindByAgeLessThanEqual", {Type: code.SimpleType("int")},
Params: []code.Param{ },
{Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, Returns: []code.Type{
{Type: code.SimpleType("string")}, code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
}, code.SimpleType("error"),
Returns: []code.Type{ },
code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, Operation: spec.FindOperation{
code.SimpleType("error"), Mode: spec.QueryModeMany,
}, Query: spec.QuerySpec{Predicates: []spec.Predicate{
Operation: spec.FindOperation{ {Field: "Age", Comparator: spec.ComparatorLessThanEqual},
Mode: spec.QueryModeMany, }},
Query: spec.QuerySpec{Predicates: []spec.Predicate{
{Field: "Age", Comparator: spec.ComparatorLessThanEqual},
}},
},
},
}, },
}, },
}, },
{ {
Name: "FindByArgGreaterThan method", Name: "FindByArgGreaterThan method",
Interface: code.Interface{ Method: code.Method{
Name: "UserRepository", Name: "FindByAgeGreaterThan",
Methods: []code.Method{ Params: []code.Param{
{ {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}},
Name: "FindByAgeGreaterThan", {Type: code.SimpleType("int")},
Params: []code.Param{ },
{Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, Returns: []code.Type{
{Type: code.SimpleType("string")}, code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
}, code.SimpleType("error"),
Returns: []code.Type{
code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
code.SimpleType("error"),
},
},
}, },
}, },
ExpectedOutput: spec.RepositorySpec{ ExpectedOutput: spec.MethodSpec{
InterfaceName: "UserRepository", Name: "FindByAgeGreaterThan",
Methods: []spec.MethodSpec{ Params: []code.Param{
{ {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}},
Name: "FindByAgeGreaterThan", {Type: code.SimpleType("int")},
Params: []code.Param{ },
{Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, Returns: []code.Type{
{Type: code.SimpleType("string")}, code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
}, code.SimpleType("error"),
Returns: []code.Type{ },
code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, Operation: spec.FindOperation{
code.SimpleType("error"), Mode: spec.QueryModeMany,
}, Query: spec.QuerySpec{Predicates: []spec.Predicate{
Operation: spec.FindOperation{ {Field: "Age", Comparator: spec.ComparatorGreaterThan},
Mode: spec.QueryModeMany, }},
Query: spec.QuerySpec{Predicates: []spec.Predicate{
{Field: "Age", Comparator: spec.ComparatorGreaterThan},
}},
},
},
}, },
}, },
}, },
{ {
Name: "FindByArgGreaterThanEqual method", Name: "FindByArgGreaterThanEqual method",
Interface: code.Interface{ Method: code.Method{
Name: "UserRepository", Name: "FindByAgeGreaterThanEqual",
Methods: []code.Method{ Params: []code.Param{
{ {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}},
Name: "FindByAgeGreaterThanEqual", {Type: code.SimpleType("int")},
Params: []code.Param{ },
{Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, Returns: []code.Type{
{Type: code.SimpleType("string")}, code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
}, code.SimpleType("error"),
Returns: []code.Type{
code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
code.SimpleType("error"),
},
},
}, },
}, },
ExpectedOutput: spec.RepositorySpec{ ExpectedOutput: spec.MethodSpec{
InterfaceName: "UserRepository", Name: "FindByAgeGreaterThanEqual",
Methods: []spec.MethodSpec{ Params: []code.Param{
{ {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}},
Name: "FindByAgeGreaterThanEqual", {Type: code.SimpleType("int")},
Params: []code.Param{ },
{Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, Returns: []code.Type{
{Type: code.SimpleType("string")}, code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
}, code.SimpleType("error"),
Returns: []code.Type{ },
code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, Operation: spec.FindOperation{
code.SimpleType("error"), Mode: spec.QueryModeMany,
}, Query: spec.QuerySpec{Predicates: []spec.Predicate{
Operation: spec.FindOperation{ {Field: "Age", Comparator: spec.ComparatorGreaterThanEqual},
Mode: spec.QueryModeMany, }},
Query: spec.QuerySpec{Predicates: []spec.Predicate{
{Field: "Age", Comparator: spec.ComparatorGreaterThanEqual},
}},
},
},
}, },
},
},
}
structModel := code.Struct{
Name: "UserModel",
Fields: code.StructFields{
{
Name: "ID",
Type: code.ExternalType{PackageAlias: "primitive", Name: "ObjectID"},
},
{
Name: "PhoneNumber",
Type: code.SimpleType("string"),
},
{
Name: "Gender",
Type: code.SimpleType("Gender"),
},
{
Name: "City",
Type: code.SimpleType("string"),
},
{
Name: "Age",
Type: code.SimpleType("int"),
}, },
}, },
} }
for _, testCase := range testTable { for _, testCase := range testTable {
t.Run(testCase.Name, func(t *testing.T) { t.Run(testCase.Name, func(t *testing.T) {
actualSpec, err := spec.ParseRepositoryInterface(structModel, testCase.Interface) actualSpec, err := spec.ParseInterfaceMethod(structModel, testCase.Method)
if err != nil { if err != nil {
t.Errorf("Error = %s", err) t.Errorf("Error = %s", err)
@ -524,3 +405,176 @@ func TestParseRepositoryInterface(t *testing.T) {
}) })
} }
} }
type ParseInterfaceMethodInvalidTestCase struct {
Name string
Method code.Method
ExpectedError error
}
func TestParseInterfaceMethodInvalid(t *testing.T) {
testTable := []ParseInterfaceMethodInvalidTestCase{
{
Name: "unknown operation",
Method: code.Method{
Name: "SearchByID",
},
ExpectedError: spec.UnknownOperationError,
},
{
Name: "unsupported find method name",
Method: code.Method{
Name: "Find",
},
ExpectedError: spec.UnsupportedNameError,
},
{
Name: "invalid number of returns",
Method: code.Method{
Name: "FindOneByID",
Returns: []code.Type{
code.SimpleType("UserModel"),
code.SimpleType("int"),
code.SimpleType("error"),
},
},
ExpectedError: spec.UnsupportedReturnError,
},
{
Name: "unsupported return values from find method",
Method: code.Method{
Name: "FindOneByID",
Returns: []code.Type{
code.SimpleType("UserModel"),
code.SimpleType("error"),
},
},
ExpectedError: spec.UnsupportedReturnError,
},
{
Name: "error return not provided",
Method: code.Method{
Name: "FindOneByID",
Returns: []code.Type{
code.SimpleType("UserModel"),
code.SimpleType("int"),
},
},
ExpectedError: spec.UnsupportedReturnError,
},
{
Name: "misplaced operator token (leftmost)",
Method: code.Method{
Name: "FindByAndGender",
Returns: []code.Type{
code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
code.SimpleType("error"),
},
},
ExpectedError: spec.InvalidQueryError,
},
{
Name: "misplaced operator token (rightmost)",
Method: code.Method{
Name: "FindByGenderAnd",
Returns: []code.Type{
code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
code.SimpleType("error"),
},
},
ExpectedError: spec.InvalidQueryError,
},
{
Name: "misplaced operator token (double operator)",
Method: code.Method{
Name: "FindByGenderAndAndCity",
Returns: []code.Type{
code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
code.SimpleType("error"),
},
},
ExpectedError: spec.InvalidQueryError,
},
{
Name: "ambiguous query",
Method: code.Method{
Name: "FindByGenderAndCityOrAge",
Returns: []code.Type{
code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
code.SimpleType("error"),
},
},
ExpectedError: spec.InvalidQueryError,
},
{
Name: "no context parameter",
Method: code.Method{
Name: "FindByGender",
Params: []code.Param{
{Type: code.SimpleType("Gender")},
},
Returns: []code.Type{
code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
code.SimpleType("error"),
},
},
ExpectedError: spec.ContextParamRequiredError,
},
{
Name: "mismatched number of parameters",
Method: code.Method{
Name: "FindByCountry",
Params: []code.Param{
{Type: code.ExternalType{PackageAlias: "context", Name: "Context"}},
{Type: code.SimpleType("string")},
{Type: code.SimpleType("string")},
},
Returns: []code.Type{
code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}},
code.SimpleType("error"),
},
},
ExpectedError: spec.InvalidParamError,
},
{
Name: "struct field not found",
Method: code.Method{
Name: "FindByCountry",
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"),
},
},
ExpectedError: spec.StructFieldNotFoundError,
},
{
Name: "mismatched method parameter type",
Method: code.Method{
Name: "FindByGender",
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"),
},
},
ExpectedError: spec.InvalidParamError,
},
}
for _, testCase := range testTable {
t.Run(testCase.Name, func(t *testing.T) {
_, err := spec.ParseInterfaceMethod(structModel, testCase.Method)
if err != testCase.ExpectedError {
t.Errorf("\nExpected = %v\nReceived = %v", testCase.ExpectedError, err)
}
})
}
}