Add functionality to parse repository specification from method name (#2)

This commit is contained in:
sunboyy 2021-01-16 13:36:44 +07:00 committed by GitHub
parent e9505d91c4
commit 3808684ed0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 458 additions and 0 deletions

2
go.mod
View file

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

View 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)
}
})
}
}