Add functionality to parse repository specification from method name (#2)
This commit is contained in:
parent
e9505d91c4
commit
3808684ed0
5 changed files with 458 additions and 0 deletions
2
go.mod
2
go.mod
|
@ -1,3 +1,5 @@
|
||||||
module github.com/sunboyy/repogen
|
module github.com/sunboyy/repogen
|
||||||
|
|
||||||
go 1.15
|
go 1.15
|
||||||
|
|
||||||
|
require github.com/fatih/camelcase v1.0.0
|
||||||
|
|
2
go.sum
Normal file
2
go.sum
Normal file
|
@ -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=
|
46
internal/spec/models.go
Normal file
46
internal/spec/models.go
Normal file
|
@ -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)
|
||||||
|
}
|
149
internal/spec/parser.go
Normal file
149
internal/spec/parser.go
Normal file
|
@ -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
|
||||||
|
}
|
259
internal/spec/parser_test.go
Normal file
259
internal/spec/parser_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue