From 3808684ed0098ce58f4d653d13e46d20c71f8ea2 Mon Sep 17 00:00:00 2001 From: sunboyy Date: Sat, 16 Jan 2021 13:36:44 +0700 Subject: [PATCH] Add functionality to parse repository specification from method name (#2) --- go.mod | 2 + go.sum | 2 + internal/spec/models.go | 46 +++++++ internal/spec/parser.go | 149 ++++++++++++++++++++ internal/spec/parser_test.go | 259 +++++++++++++++++++++++++++++++++++ 5 files changed, 458 insertions(+) create mode 100644 go.sum create mode 100644 internal/spec/models.go create mode 100644 internal/spec/parser.go create mode 100644 internal/spec/parser_test.go diff --git a/go.mod b/go.mod index 499fb3f..2548e97 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/sunboyy/repogen go 1.15 + +require github.com/fatih/camelcase v1.0.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..315a92c --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= +github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= diff --git a/internal/spec/models.go b/internal/spec/models.go new file mode 100644 index 0000000..11e9cb9 --- /dev/null +++ b/internal/spec/models.go @@ -0,0 +1,46 @@ +package spec + +import "github.com/sunboyy/repogen/internal/code" + +// QueryMode one or many +type QueryMode string + +// query mode constants +const ( + QueryModeOne QueryMode = "ONE" + 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 + Params []code.Param + Returns []code.Type + Operation Operation +} + +// Operation is an interface for any kind of operation +type Operation interface { +} + +// FindOperation is a method specification for find operations +type FindOperation struct { + Mode QueryMode + Query Query +} + +// Query is a condition of querying the database +type Query struct { + Fields []string +} + +// NumberOfArguments returns number of arguments required to perform the query +func (q Query) NumberOfArguments() int { + return len(q.Fields) +} diff --git a/internal/spec/parser.go b/internal/spec/parser.go new file mode 100644 index 0000000..3369195 --- /dev/null +++ b/internal/spec/parser.go @@ -0,0 +1,149 @@ +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{ + StructModel: structModel, + Interface: intf, + } + + return parser.Parse() +} + +type repositoryInterfaceParser struct { + StructModel code.Struct + Interface code.Interface +} + +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) + switch methodNameTokens[0] { + case "Find": + return p.parseFindMethod(method, methodNameTokens[1:]) + } + return MethodSpec{}, errors.New("method name not supported") +} + +func (p repositoryInterfaceParser) parseFindMethod(method code.Method, tokens []string) (MethodSpec, error) { + if len(tokens) == 0 { + return MethodSpec{}, errors.New("method name not supported") + } + + mode, err := p.extractFindReturns(method.Returns) + if err != nil { + return MethodSpec{}, err + } + + query, err := p.parseQuery(tokens) + if err != nil { + return MethodSpec{}, err + } + + if query.NumberOfArguments()+1 != len(method.Params) { + return MethodSpec{}, errors.New("method parameter not supported") + } + + return MethodSpec{ + Name: method.Name, + Params: method.Params, + Returns: method.Returns, + Operation: FindOperation{ + Mode: mode, + Query: query, + }, + }, nil +} + +func (p repositoryInterfaceParser) extractFindReturns(returns []code.Type) (QueryMode, error) { + if len(returns) != 2 { + return "", errors.New("method return not supported") + } + + if returns[1] != code.SimpleType("error") { + return "", errors.New("method return not supported") + } + + pointerType, ok := returns[0].(code.PointerType) + if ok { + simpleType := pointerType.ContainedType + if simpleType == code.SimpleType(p.StructModel.Name) { + return QueryModeOne, nil + } + return "", fmt.Errorf("invalid return type %s", pointerType.Code()) + } + + arrayType, ok := returns[0].(code.ArrayType) + if ok { + pointerType, ok := arrayType.ContainedType.(code.PointerType) + if ok { + simpleType := pointerType.ContainedType + if simpleType == code.SimpleType(p.StructModel.Name) { + return QueryModeMany, nil + } + return "", fmt.Errorf("invalid return type %s", pointerType.Code()) + } + } + + return "", errors.New("method return not supported") +} + +func (p repositoryInterfaceParser) parseQuery(tokens []string) (Query, error) { + if len(tokens) == 0 { + return Query{}, errors.New("method name not supported") + } + + if len(tokens) == 1 && tokens[0] == "All" { + return Query{}, nil + } + + if tokens[0] == "One" { + tokens = tokens[1:] + } + if tokens[0] == "By" { + tokens = tokens[1:] + } + + if tokens[0] == "And" { + return Query{}, errors.New("method name not supported") + } + var queryFields []string + var aggregatedToken string + for _, token := range tokens { + if token != "And" { + aggregatedToken += token + } else { + queryFields = append(queryFields, aggregatedToken) + aggregatedToken = "" + } + } + if aggregatedToken == "" { + return Query{}, errors.New("method name not supported") + } + queryFields = append(queryFields, aggregatedToken) + + return Query{Fields: queryFields}, nil +} diff --git a/internal/spec/parser_test.go b/internal/spec/parser_test.go new file mode 100644 index 0000000..56160c3 --- /dev/null +++ b/internal/spec/parser_test.go @@ -0,0 +1,259 @@ +package spec_test + +import ( + "reflect" + "testing" + + "github.com/sunboyy/repogen/internal/code" + "github.com/sunboyy/repogen/internal/spec" +) + +type TestCase struct { + Name string + Interface code.Interface + ExpectedOutput spec.RepositorySpec +} + +func TestParseRepositoryInterface(t *testing.T) { + testTable := []TestCase{ + { + Name: "interface spec", + Interface: code.Interface{ + Name: "UserRepository", + }, + ExpectedOutput: spec.RepositorySpec{ + InterfaceName: "UserRepository", + }, + }, + { + 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"), + }, + }, + }, + }, + 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.Query{Fields: []string{"ID"}}, + }, + }, + }, + }, + }, + { + 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"), + }, + }, + }, + }, + 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.Query{Fields: []string{"PhoneNumber"}}, + }, + }, + }, + }, + }, + { + 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"), + }, + }, + }, + }, + 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.Query{Fields: []string{"City"}}, + }, + }, + }, + }, + }, + { + 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"), + }, + }, + }, + }, + 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, + }, + }, + }, + }, + }, + { + 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"), + }, + }, + }, + }, + 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.Query{Fields: []string{"City", "Gender"}}, + }, + }, + }, + }, + }, + } + + 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"), + }, + }, + } + + for _, testCase := range testTable { + t.Run(testCase.Name, func(t *testing.T) { + actualSpec, err := spec.ParseRepositoryInterface(structModel, testCase.Interface) + + if err != nil { + t.Errorf("Error = %s", err) + } + if !reflect.DeepEqual(actualSpec, testCase.ExpectedOutput) { + t.Errorf("Expected = %v\nReceived = %v", testCase.ExpectedOutput, actualSpec) + } + }) + } +}