From f799a284b1f2d08f549b376335cc6b475a48cc1a Mon Sep 17 00:00:00 2001 From: sunboyy Date: Thu, 21 Jan 2021 18:56:30 +0700 Subject: [PATCH] Validate parameter types in method signature --- codecov.yml | 2 +- internal/mongo/generator.go | 30 +- internal/mongo/generator_test.go | 4 +- internal/spec/errors.go | 35 ++ internal/spec/models.go | 6 - internal/spec/parser.go | 106 ++-- internal/spec/parser_test.go | 892 ++++++++++++++++--------------- 7 files changed, 587 insertions(+), 488 deletions(-) create mode 100644 internal/spec/errors.go diff --git a/codecov.yml b/codecov.yml index 20f9e22..83c628e 100644 --- a/codecov.yml +++ b/codecov.yml @@ -2,7 +2,7 @@ coverage: status: project: default: - target: 65% + target: 70% threshold: 5% patch: default: diff --git a/internal/mongo/generator.go b/internal/mongo/generator.go index 234e5ab..c127871 100644 --- a/internal/mongo/generator.go +++ b/internal/mongo/generator.go @@ -13,15 +13,20 @@ import ( // GenerateMongoRepository generates mongodb repository func GenerateMongoRepository(packageName string, structModel code.Struct, intf code.Interface) (string, error) { - repositorySpec, err := spec.ParseRepositoryInterface(structModel, intf) - if err != nil { - return "", err + var methodSpecs []spec.MethodSpec + for _, method := range intf.Methods { + methodSpec, err := spec.ParseInterfaceMethod(structModel, method) + if err != nil { + return "", err + } + methodSpecs = append(methodSpecs, methodSpec) } generator := mongoRepositoryGenerator{ - PackageName: packageName, - StructModel: structModel, - RepositorySpec: repositorySpec, + PackageName: packageName, + StructModel: structModel, + InterfaceName: intf.Name, + MethodSpecs: methodSpecs, } output, err := generator.Generate() @@ -33,9 +38,10 @@ func GenerateMongoRepository(packageName string, structModel code.Struct, intf c } type mongoRepositoryGenerator struct { - PackageName string - StructModel code.Struct - RepositorySpec spec.RepositorySpec + PackageName string + StructModel code.Struct + InterfaceName string + MethodSpecs []spec.MethodSpec } func (g mongoRepositoryGenerator) Generate() (string, error) { @@ -44,7 +50,7 @@ func (g mongoRepositoryGenerator) Generate() (string, error) { return "", err } - for _, method := range g.RepositorySpec.Methods { + for _, method := range g.MethodSpecs { if err := g.generateMethod(buffer, method); err != nil { return "", err } @@ -66,7 +72,7 @@ func (g mongoRepositoryGenerator) generateBaseContent(buffer *bytes.Buffer) erro tmplData := mongoBaseTemplateData{ PackageName: g.PackageName, - InterfaceName: g.RepositorySpec.InterfaceName, + InterfaceName: g.InterfaceName, StructName: g.structName(), } @@ -167,5 +173,5 @@ func (g mongoRepositoryGenerator) generateFindImplementation(operation spec.Find } func (g mongoRepositoryGenerator) structName() string { - return g.RepositorySpec.InterfaceName + "Mongo" + return g.InterfaceName + "Mongo" } diff --git a/internal/mongo/generator_test.go b/internal/mongo/generator_test.go index 7f28719..8c38357 100644 --- a/internal/mongo/generator_test.go +++ b/internal/mongo/generator_test.go @@ -83,7 +83,7 @@ func TestGenerateMongoRepository(t *testing.T) { Name: "FindByGenderNot", Params: []code.Param{ {Name: "ctx", Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, - {Name: "gender", Type: code.SimpleType("int")}, + {Name: "gender", Type: code.SimpleType("Gender")}, }, Returns: []code.Type{ 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 } -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{ "gender": bson.M{"$ne": arg0}, }) diff --git a/internal/spec/errors.go b/internal/spec/errors.go new file mode 100644 index 0000000..22f3495 --- /dev/null +++ b/internal/spec/errors.go @@ -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" +) diff --git a/internal/spec/models.go b/internal/spec/models.go index e0143f1..472f050 100644 --- a/internal/spec/models.go +++ b/internal/spec/models.go @@ -15,12 +15,6 @@ const ( 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 type MethodSpec struct { Name string diff --git a/internal/spec/parser.go b/internal/spec/parser.go index fa83714..c9d75b7 100644 --- a/internal/spec/parser.go +++ b/internal/spec/parser.go @@ -1,59 +1,40 @@ package spec import ( - "errors" - "fmt" - "github.com/fatih/camelcase" "github.com/sunboyy/repogen/internal/code" ) -// ParseRepositoryInterface returns repository spec from declared repository interface -func ParseRepositoryInterface(structModel code.Struct, intf code.Interface) (RepositorySpec, error) { - parser := repositoryInterfaceParser{ +// ParseInterfaceMethod returns repository method spec from declared interface method +func ParseInterfaceMethod(structModel code.Struct, method code.Method) (MethodSpec, error) { + parser := interfaceMethodParser{ StructModel: structModel, - Interface: intf, + Method: method, } return parser.Parse() } -type repositoryInterfaceParser struct { +type interfaceMethodParser struct { StructModel code.Struct - Interface code.Interface + Method code.Method } -func (p repositoryInterfaceParser) Parse() (RepositorySpec, error) { - repositorySpec := RepositorySpec{ - 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) +func (p interfaceMethodParser) Parse() (MethodSpec, error) { + methodNameTokens := camelcase.Split(p.Method.Name) switch methodNameTokens[0] { 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 { - 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 { return MethodSpec{}, err } @@ -63,14 +44,14 @@ func (p repositoryInterfaceParser) parseFindMethod(method code.Method, tokens [] return MethodSpec{}, err } - if querySpec.NumberOfArguments()+1 != len(method.Params) { - return MethodSpec{}, errors.New("method parameter not supported") + if err := p.validateMethodSignature(querySpec); err != nil { + return MethodSpec{}, err } return MethodSpec{ - Name: method.Name, - Params: method.Params, - Returns: method.Returns, + Name: p.Method.Name, + Params: p.Method.Params, + Returns: p.Method.Returns, Operation: FindOperation{ Mode: mode, Query: querySpec, @@ -78,13 +59,13 @@ func (p repositoryInterfaceParser) parseFindMethod(method code.Method, tokens [] }, nil } -func (p repositoryInterfaceParser) extractFindReturns(returns []code.Type) (QueryMode, error) { +func (p interfaceMethodParser) extractFindReturns(returns []code.Type) (QueryMode, error) { if len(returns) != 2 { - return "", errors.New("method return not supported") + return "", UnsupportedReturnError } if returns[1] != code.SimpleType("error") { - return "", errors.New("method return not supported") + return "", UnsupportedReturnError } 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) { return QueryModeOne, nil } - return "", fmt.Errorf("invalid return type %s", pointerType.Code()) + return "", UnsupportedReturnError } 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) { 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 { - return QuerySpec{}, errors.New("method name not supported") + return QuerySpec{}, InvalidQueryError } 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" { - return QuerySpec{}, errors.New("method name not supported") + return QuerySpec{}, InvalidQueryError } var operator Operator @@ -137,6 +118,8 @@ func (p repositoryInterfaceParser) parseQuery(tokens []string) (QuerySpec, error for _, token := range tokens { if token != "And" && token != "Or" { aggregatedToken = append(aggregatedToken, token) + } else if len(aggregatedToken) == 0 { + return QuerySpec{}, InvalidQueryError } else if token == "And" && operator != OperatorOr { operator = OperatorAnd predicates = append(predicates, aggregatedToken.ToPredicate()) @@ -146,13 +129,40 @@ func (p repositoryInterfaceParser) parseQuery(tokens []string) (QuerySpec, error predicates = append(predicates, aggregatedToken.ToPredicate()) aggregatedToken = predicateToken{} } else { - return QuerySpec{}, errors.New("method name contains ambiguous query") + return QuerySpec{}, InvalidQueryError } } if len(aggregatedToken) == 0 { - return QuerySpec{}, errors.New("method name not supported") + return QuerySpec{}, InvalidQueryError } predicates = append(predicates, aggregatedToken.ToPredicate()) 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 +} diff --git a/internal/spec/parser_test.go b/internal/spec/parser_test.go index f365525..bd60043 100644 --- a/internal/spec/parser_test.go +++ b/internal/spec/parser_test.go @@ -8,224 +8,191 @@ import ( "github.com/sunboyy/repogen/internal/spec" ) -type TestCase struct { - Name string - Interface code.Interface - ExpectedOutput spec.RepositorySpec -} - -func TestParseRepositoryInterface(t *testing.T) { - testTable := []TestCase{ +var structModel = code.Struct{ + Name: "UserModel", + Fields: code.StructFields{ { - Name: "interface spec", - Interface: code.Interface{ - Name: "UserRepository", - }, - ExpectedOutput: spec.RepositorySpec{ - InterfaceName: "UserRepository", - }, + 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"), + }, + }, +} + +type ParseInterfaceMethodTestCase struct { + Name string + Method code.Method + ExpectedOutput spec.MethodSpec +} + +func TestParseInterfaceMethod(t *testing.T) { + testTable := []ParseInterfaceMethodTestCase{ { Name: "FindOneByArg method", - Interface: code.Interface{ - Name: "UserRepository", - Methods: []code.Method{ - { - Name: "FindOneByID", - Params: []code.Param{ - {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, - {Type: code.ExternalType{PackageAlias: "primitive", Name: "ObjectID"}}, - }, - Returns: []code.Type{ - code.PointerType{ContainedType: code.SimpleType("UserModel")}, - code.SimpleType("error"), - }, - }, + Method: code.Method{ + Name: "FindOneByID", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.ExternalType{PackageAlias: "primitive", Name: "ObjectID"}}, + }, + Returns: []code.Type{ + code.PointerType{ContainedType: code.SimpleType("UserModel")}, + code.SimpleType("error"), }, }, - ExpectedOutput: spec.RepositorySpec{ - InterfaceName: "UserRepository", - Methods: []spec.MethodSpec{ - { - Name: "FindOneByID", - Params: []code.Param{ - {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, - {Type: code.ExternalType{PackageAlias: "primitive", Name: "ObjectID"}}, - }, - Returns: []code.Type{ - code.PointerType{ContainedType: code.SimpleType("UserModel")}, - code.SimpleType("error"), - }, - Operation: spec.FindOperation{ - Mode: spec.QueryModeOne, - Query: spec.QuerySpec{Predicates: []spec.Predicate{ - {Field: "ID", Comparator: spec.ComparatorEqual}, - }}, - }, - }, + ExpectedOutput: spec.MethodSpec{ + Name: "FindOneByID", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.ExternalType{PackageAlias: "primitive", Name: "ObjectID"}}, + }, + Returns: []code.Type{ + code.PointerType{ContainedType: code.SimpleType("UserModel")}, + code.SimpleType("error"), + }, + Operation: spec.FindOperation{ + Mode: spec.QueryModeOne, + Query: spec.QuerySpec{Predicates: []spec.Predicate{ + {Field: "ID", Comparator: spec.ComparatorEqual}, + }}, }, }, }, { Name: "FindOneByMultiWordArg method", - Interface: code.Interface{ - Name: "UserRepository", - Methods: []code.Method{ - { - Name: "FindOneByPhoneNumber", - Params: []code.Param{ - {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, - {Type: code.SimpleType("string")}, - }, - Returns: []code.Type{ - code.PointerType{ContainedType: code.SimpleType("UserModel")}, - code.SimpleType("error"), - }, - }, + Method: code.Method{ + Name: "FindOneByPhoneNumber", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + }, + Returns: []code.Type{ + code.PointerType{ContainedType: code.SimpleType("UserModel")}, + code.SimpleType("error"), }, }, - ExpectedOutput: spec.RepositorySpec{ - InterfaceName: "UserRepository", - Methods: []spec.MethodSpec{ - { - Name: "FindOneByPhoneNumber", - Params: []code.Param{ - {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, - {Type: code.SimpleType("string")}, - }, - Returns: []code.Type{ - code.PointerType{ContainedType: code.SimpleType("UserModel")}, - code.SimpleType("error"), - }, - Operation: spec.FindOperation{ - Mode: spec.QueryModeOne, - Query: spec.QuerySpec{Predicates: []spec.Predicate{ - {Field: "PhoneNumber", Comparator: spec.ComparatorEqual}, - }}, - }, - }, + ExpectedOutput: spec.MethodSpec{ + Name: "FindOneByPhoneNumber", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + }, + Returns: []code.Type{ + code.PointerType{ContainedType: code.SimpleType("UserModel")}, + code.SimpleType("error"), + }, + Operation: spec.FindOperation{ + Mode: spec.QueryModeOne, + Query: spec.QuerySpec{Predicates: []spec.Predicate{ + {Field: "PhoneNumber", Comparator: spec.ComparatorEqual}, + }}, }, }, }, { Name: "FindByArg method", - Interface: code.Interface{ - Name: "UserRepository", - Methods: []code.Method{ - { - Name: "FindByCity", - 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"), - }, - }, + Method: code.Method{ + Name: "FindByCity", + 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: "FindByCity", - 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", Comparator: spec.ComparatorEqual}, - }}, - }, - }, + ExpectedOutput: spec.MethodSpec{ + Name: "FindByCity", + 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", Comparator: spec.ComparatorEqual}, + }}, }, }, }, { Name: "FindAll method", - Interface: code.Interface{ - Name: "UserRepository", - Methods: []code.Method{ - { - Name: "FindAll", - Params: []code.Param{ - {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, - }, - Returns: []code.Type{ - code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, - code.SimpleType("error"), - }, - }, + Method: code.Method{ + Name: "FindAll", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + }, + Returns: []code.Type{ + code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, + code.SimpleType("error"), }, }, - ExpectedOutput: spec.RepositorySpec{ - InterfaceName: "UserRepository", - Methods: []spec.MethodSpec{ - { - Name: "FindAll", - Params: []code.Param{ - {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, - }, - Returns: []code.Type{ - code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, - code.SimpleType("error"), - }, - Operation: spec.FindOperation{ - Mode: spec.QueryModeMany, - }, - }, + ExpectedOutput: spec.MethodSpec{ + Name: "FindAll", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + }, + Returns: []code.Type{ + code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, + code.SimpleType("error"), + }, + Operation: spec.FindOperation{ + Mode: spec.QueryModeMany, }, }, }, { Name: "FindByArgAndArg method", - Interface: code.Interface{ - Name: "UserRepository", - Methods: []code.Method{ - { - Name: "FindByCityAndGender", - Params: []code.Param{ - {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, - {Type: code.SimpleType("string")}, - {Type: code.SimpleType("Gender")}, - }, - Returns: []code.Type{ - code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, - code.SimpleType("error"), - }, - }, + Method: code.Method{ + Name: "FindByCityAndGender", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + {Type: code.SimpleType("Gender")}, + }, + Returns: []code.Type{ + code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, + code.SimpleType("error"), }, }, - ExpectedOutput: spec.RepositorySpec{ - InterfaceName: "UserRepository", - Methods: []spec.MethodSpec{ - { - Name: "FindByCityAndGender", - Params: []code.Param{ - {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, - {Type: code.SimpleType("string")}, - {Type: code.SimpleType("Gender")}, - }, - Returns: []code.Type{ - code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, - code.SimpleType("error"), - }, - Operation: spec.FindOperation{ - Mode: spec.QueryModeMany, - Query: spec.QuerySpec{ - Operator: spec.OperatorAnd, - Predicates: []spec.Predicate{ - {Field: "City", Comparator: spec.ComparatorEqual}, - {Field: "Gender", Comparator: spec.ComparatorEqual}, - }, - }, + ExpectedOutput: spec.MethodSpec{ + Name: "FindByCityAndGender", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + {Type: code.SimpleType("Gender")}, + }, + Returns: []code.Type{ + code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, + code.SimpleType("error"), + }, + Operation: spec.FindOperation{ + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{ + Operator: spec.OperatorAnd, + 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", - Interface: code.Interface{ - Name: "UserRepository", - Methods: []code.Method{ - { - Name: "FindByCityOrGender", - Params: []code.Param{ - {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, - {Type: code.SimpleType("string")}, - {Type: code.SimpleType("Gender")}, - }, - Returns: []code.Type{ - code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, - code.SimpleType("error"), - }, - }, + Method: code.Method{ + Name: "FindByCityOrGender", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + {Type: code.SimpleType("Gender")}, + }, + Returns: []code.Type{ + code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, + code.SimpleType("error"), }, }, - ExpectedOutput: spec.RepositorySpec{ - InterfaceName: "UserRepository", - Methods: []spec.MethodSpec{ - { - Name: "FindByCityOrGender", - Params: []code.Param{ - {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, - {Type: code.SimpleType("string")}, - {Type: code.SimpleType("Gender")}, - }, - Returns: []code.Type{ - code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, - code.SimpleType("error"), - }, - Operation: spec.FindOperation{ - Mode: spec.QueryModeMany, - Query: spec.QuerySpec{ - Operator: spec.OperatorOr, - Predicates: []spec.Predicate{ - {Field: "City", Comparator: spec.ComparatorEqual}, - {Field: "Gender", Comparator: spec.ComparatorEqual}, - }, - }, + ExpectedOutput: spec.MethodSpec{ + Name: "FindByCityOrGender", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + {Type: code.SimpleType("Gender")}, + }, + Returns: []code.Type{ + code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, + code.SimpleType("error"), + }, + Operation: spec.FindOperation{ + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{ + Operator: spec.OperatorOr, + 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", - 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"), - }, - }, + Method: 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", Comparator: spec.ComparatorNot}, - }}, - }, - }, + ExpectedOutput: 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", Comparator: spec.ComparatorNot}, + }}, }, }, }, { 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"), - }, - }, + Method: code.Method{ + Name: "FindByAgeLessThan", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("int")}, + }, + 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", Comparator: spec.ComparatorLessThan}, - }}, - }, - }, + ExpectedOutput: spec.MethodSpec{ + Name: "FindByAgeLessThan", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("int")}, + }, + 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", Comparator: spec.ComparatorLessThan}, + }}, }, }, }, { 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"), - }, - }, + Method: code.Method{ + Name: "FindByAgeLessThanEqual", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("int")}, + }, + 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", Comparator: spec.ComparatorLessThanEqual}, - }}, - }, - }, + ExpectedOutput: spec.MethodSpec{ + Name: "FindByAgeLessThanEqual", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("int")}, + }, + 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", Comparator: spec.ComparatorLessThanEqual}, + }}, }, }, }, { 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"), - }, - }, + Method: code.Method{ + Name: "FindByAgeGreaterThan", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("int")}, + }, + 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", Comparator: spec.ComparatorGreaterThan}, - }}, - }, - }, + ExpectedOutput: spec.MethodSpec{ + Name: "FindByAgeGreaterThan", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("int")}, + }, + 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", Comparator: spec.ComparatorGreaterThan}, + }}, }, }, }, { 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"), - }, - }, + Method: code.Method{ + Name: "FindByAgeGreaterThanEqual", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("int")}, + }, + 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", Comparator: spec.ComparatorGreaterThanEqual}, - }}, - }, - }, + ExpectedOutput: spec.MethodSpec{ + Name: "FindByAgeGreaterThanEqual", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("int")}, + }, + 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", 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 { 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 { 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) + } + }) + } +}