diff --git a/codecov.yml b/codecov.yml index b93815e..080f560 100644 --- a/codecov.yml +++ b/codecov.yml @@ -3,7 +3,7 @@ coverage: project: default: target: 75% - threshold: 5% + threshold: 4% patch: default: target: 50% diff --git a/internal/mongo/errors.go b/internal/mongo/errors.go new file mode 100644 index 0000000..b9e1e8e --- /dev/null +++ b/internal/mongo/errors.go @@ -0,0 +1,20 @@ +package mongo + +// GenerationError is an error from generating MongoDB repository +type GenerationError string + +func (err GenerationError) Error() string { + switch err { + case OperationNotSupportedError: + return "operation not supported" + case BsonTagNotFoundError: + return "bson tag not found" + } + return string(err) +} + +// generation error constants +const ( + OperationNotSupportedError GenerationError = "ERROR_OPERATION_NOT_SUPPORTED" + BsonTagNotFoundError GenerationError = "ERROR_BSON_TAG_NOT_FOUND" +) diff --git a/internal/mongo/generator.go b/internal/mongo/generator.go index 31ea396..7c1aade 100644 --- a/internal/mongo/generator.go +++ b/internal/mongo/generator.go @@ -2,7 +2,6 @@ package mongo import ( "bytes" - "errors" "fmt" "io" "text/template" @@ -80,35 +79,24 @@ func (g RepositoryGenerator) generateMethodImplementation(methodSpec spec.Method switch operation := methodSpec.Operation.(type) { case spec.FindOperation: return g.generateFindImplementation(operation) + case spec.DeleteOperation: + return g.generateDeleteImplementation(operation) } - return "", errors.New("method spec not supported") + return "", OperationNotSupportedError } func (g RepositoryGenerator) generateFindImplementation(operation spec.FindOperation) (string, error) { buffer := new(bytes.Buffer) - var predicates []predicate - for _, predicateSpec := range operation.Query.Predicates { - structField, ok := g.StructModel.Fields.ByName(predicateSpec.Field) - if !ok { - return "", fmt.Errorf("struct field %s not found", predicateSpec.Field) - } - - bsonTag, ok := structField.Tags["bson"] - if !ok { - return "", fmt.Errorf("struct field %s does not have bson tag", predicateSpec.Field) - } - - predicates = append(predicates, predicate{Field: bsonTag[0], Comparator: predicateSpec.Comparator}) + querySpec, err := g.mongoQuerySpec(operation.Query) + if err != nil { + return "", err } tmplData := mongoFindTemplateData{ EntityType: g.StructModel.Name, - QuerySpec: querySpec{ - Operator: operation.Query.Operator, - Predicates: predicates, - }, + QuerySpec: querySpec, } if operation.Mode == spec.QueryModeOne { @@ -134,6 +122,64 @@ func (g RepositoryGenerator) generateFindImplementation(operation spec.FindOpera return buffer.String(), nil } +func (g RepositoryGenerator) generateDeleteImplementation(operation spec.DeleteOperation) (string, error) { + buffer := new(bytes.Buffer) + + querySpec, err := g.mongoQuerySpec(operation.Query) + if err != nil { + return "", err + } + + tmplData := mongoDeleteTemplateData{ + QuerySpec: querySpec, + } + + if operation.Mode == spec.QueryModeOne { + tmpl, err := template.New("mongo_repository_deleteone").Parse(deleteOneTemplate) + if err != nil { + return "", err + } + + if err := tmpl.Execute(buffer, tmplData); err != nil { + return "", err + } + } else { + tmpl, err := template.New("mongo_repository_deletemany").Parse(deleteManyTemplate) + if err != nil { + return "", err + } + + if err := tmpl.Execute(buffer, tmplData); err != nil { + return "", err + } + } + + return buffer.String(), nil +} + +func (g RepositoryGenerator) mongoQuerySpec(query spec.QuerySpec) (querySpec, error) { + var predicates []predicate + + for _, predicateSpec := range query.Predicates { + structField, ok := g.StructModel.Fields.ByName(predicateSpec.Field) + if !ok { + return querySpec{}, fmt.Errorf("struct field %s not found", predicateSpec.Field) + } + + bsonTag, ok := structField.Tags["bson"] + if !ok { + return querySpec{}, BsonTagNotFoundError + } + + predicates = append(predicates, predicate{Field: bsonTag[0], Comparator: predicateSpec.Comparator}) + } + + return querySpec{ + Operator: query.Operator, + Predicates: predicates, + }, nil +} + func (g RepositoryGenerator) structName() string { return g.InterfaceName + "Mongo" } diff --git a/internal/mongo/generator_test.go b/internal/mongo/generator_test.go index 0579e15..14ae9b8 100644 --- a/internal/mongo/generator_test.go +++ b/internal/mongo/generator_test.go @@ -10,6 +10,36 @@ import ( "github.com/sunboyy/repogen/internal/testutils" ) +var userModel = code.Struct{ + Name: "UserModel", + Fields: code.StructFields{ + { + Name: "ID", + Type: code.ExternalType{PackageAlias: "primitive", Name: "ObjectID"}, + Tags: map[string][]string{"bson": {"_id", "omitempty"}}, + }, + { + Name: "Username", + Type: code.SimpleType("string"), + Tags: map[string][]string{"bson": {"username"}}, + }, + { + Name: "Gender", + Type: code.SimpleType("Gender"), + Tags: map[string][]string{"bson": {"gender"}}, + }, + { + Name: "Age", + Type: code.SimpleType("int"), + Tags: map[string][]string{"bson": {"age"}}, + }, + { + Name: "AccessToken", + Type: code.SimpleType("string"), + }, + }, +} + const expectedConstructorResult = ` import ( "context" @@ -31,31 +61,6 @@ type UserRepositoryMongo struct { ` func TestGenerateConstructor(t *testing.T) { - userModel := code.Struct{ - Name: "UserModel", - Fields: code.StructFields{ - { - Name: "ID", - Type: code.ExternalType{PackageAlias: "primitive", Name: "ObjectID"}, - Tags: map[string][]string{"bson": {"_id", "omitempty"}}, - }, - { - Name: "Username", - Type: code.SimpleType("string"), - Tags: map[string][]string{"bson": {"username"}}, - }, - { - Name: "Gender", - Type: code.SimpleType("Gender"), - Tags: map[string][]string{"bson": {"gender"}}, - }, - { - Name: "Age", - Type: code.SimpleType("int"), - Tags: map[string][]string{"bson": {"age"}}, - }, - }, - } generator := mongo.NewGenerator(userModel, "UserRepository") buffer := new(bytes.Buffer) @@ -75,7 +80,7 @@ type GenerateMethodTestCase struct { ExpectedCode string } -func TestGenerateMethod(t *testing.T) { +func TestGenerateMethod_Find(t *testing.T) { testTable := []GenerateMethodTestCase{ { Name: "simple find one method", @@ -489,33 +494,9 @@ func (r *UserRepositoryMongo) FindByGenderIn(ctx context.Context, arg0 []Gender) `, }, } + for _, testCase := range testTable { t.Run(testCase.Name, func(t *testing.T) { - userModel := code.Struct{ - Name: "UserModel", - Fields: code.StructFields{ - { - Name: "ID", - Type: code.ExternalType{PackageAlias: "primitive", Name: "ObjectID"}, - Tags: map[string][]string{"bson": {"_id", "omitempty"}}, - }, - { - Name: "Username", - Type: code.SimpleType("string"), - Tags: map[string][]string{"bson": {"username"}}, - }, - { - Name: "Gender", - Type: code.SimpleType("Gender"), - Tags: map[string][]string{"bson": {"gender"}}, - }, - { - Name: "Age", - Type: code.SimpleType("int"), - Tags: map[string][]string{"bson": {"age"}}, - }, - }, - } generator := mongo.NewGenerator(userModel, "UserRepository") buffer := new(bytes.Buffer) @@ -530,3 +511,458 @@ func (r *UserRepositoryMongo) FindByGenderIn(ctx context.Context, arg0 []Gender) }) } } + +func TestGenerateMethod_Delete(t *testing.T) { + testTable := []GenerateMethodTestCase{ + { + Name: "simple delete one method", + MethodSpec: spec.MethodSpec{ + Name: "DeleteByID", + Params: []code.Param{ + {Name: "ctx", Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Name: "id", Type: code.ExternalType{PackageAlias: "primitive", Name: "ObjectID"}}, + }, + Returns: []code.Type{code.SimpleType("bool"), code.SimpleType("error")}, + Operation: spec.DeleteOperation{ + Mode: spec.QueryModeOne, + Query: spec.QuerySpec{ + Predicates: []spec.Predicate{ + {Comparator: spec.ComparatorEqual, Field: "ID"}, + }, + }, + }, + }, + ExpectedCode: ` +func (r *UserRepositoryMongo) DeleteByID(ctx context.Context, arg0 primitive.ObjectID) (bool, error) { + result, err := r.collection.DeleteOne(ctx, bson.M{ + "_id": arg0, + }) + if err != nil { + return false, err + } + return result.DeletedCount > 0, nil +} +`, + }, + { + Name: "simple delete many method", + MethodSpec: spec.MethodSpec{ + Name: "DeleteByGender", + Params: []code.Param{ + {Name: "ctx", Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Name: "gender", Type: code.SimpleType("Gender")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + Operation: spec.DeleteOperation{ + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{ + Predicates: []spec.Predicate{ + {Comparator: spec.ComparatorEqual, Field: "Gender"}, + }, + }, + }, + }, + ExpectedCode: ` +func (r *UserRepositoryMongo) DeleteByGender(ctx context.Context, arg0 Gender) (int, error) { + result, err := r.collection.DeleteMany(ctx, bson.M{ + "gender": arg0, + }) + if err != nil { + return 0, err + } + return int(result.DeletedCount), nil +} +`, + }, + { + Name: "delete with And operator", + MethodSpec: spec.MethodSpec{ + Name: "DeleteByGenderAndAge", + Params: []code.Param{ + {Name: "ctx", Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Name: "gender", Type: code.SimpleType("Gender")}, + {Name: "age", Type: code.SimpleType("int")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + Operation: spec.DeleteOperation{ + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{ + Operator: spec.OperatorAnd, + Predicates: []spec.Predicate{ + {Comparator: spec.ComparatorEqual, Field: "Gender"}, + {Comparator: spec.ComparatorEqual, Field: "Age"}, + }, + }, + }, + }, + ExpectedCode: ` +func (r *UserRepositoryMongo) DeleteByGenderAndAge(ctx context.Context, arg0 Gender, arg1 int) (int, error) { + result, err := r.collection.DeleteMany(ctx, bson.M{ + "gender": arg0, + "age": arg1, + }) + if err != nil { + return 0, err + } + return int(result.DeletedCount), nil +} +`, + }, + { + Name: "delete with Or operator", + MethodSpec: spec.MethodSpec{ + Name: "DeleteByGenderOrAge", + Params: []code.Param{ + {Name: "ctx", Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Name: "gender", Type: code.SimpleType("Gender")}, + {Name: "age", Type: code.SimpleType("int")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + Operation: spec.DeleteOperation{ + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{ + Operator: spec.OperatorOr, + Predicates: []spec.Predicate{ + {Comparator: spec.ComparatorEqual, Field: "Gender"}, + {Comparator: spec.ComparatorEqual, Field: "Age"}, + }, + }, + }, + }, + ExpectedCode: ` +func (r *UserRepositoryMongo) DeleteByGenderOrAge(ctx context.Context, arg0 Gender, arg1 int) (int, error) { + result, err := r.collection.DeleteMany(ctx, bson.M{ + "$or": []bson.M{ + {"gender": arg0}, + {"age": arg1}, + }, + }) + if err != nil { + return 0, err + } + return int(result.DeletedCount), nil +} +`, + }, + { + Name: "delete with Not comparator", + MethodSpec: spec.MethodSpec{ + Name: "DeleteByGenderNot", + Params: []code.Param{ + {Name: "ctx", Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Name: "gender", Type: code.SimpleType("Gender")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + Operation: spec.DeleteOperation{ + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{ + Predicates: []spec.Predicate{ + {Comparator: spec.ComparatorNot, Field: "Gender"}, + }, + }, + }, + }, + ExpectedCode: ` +func (r *UserRepositoryMongo) DeleteByGenderNot(ctx context.Context, arg0 Gender) (int, error) { + result, err := r.collection.DeleteMany(ctx, bson.M{ + "gender": bson.M{"$ne": arg0}, + }) + if err != nil { + return 0, err + } + return int(result.DeletedCount), nil +} +`, + }, + { + Name: "delete with LessThan comparator", + MethodSpec: spec.MethodSpec{ + Name: "DeleteByAgeLessThan", + Params: []code.Param{ + {Name: "ctx", Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Name: "age", Type: code.SimpleType("int")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + Operation: spec.DeleteOperation{ + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{ + Predicates: []spec.Predicate{ + {Comparator: spec.ComparatorLessThan, Field: "Age"}, + }, + }, + }, + }, + ExpectedCode: ` +func (r *UserRepositoryMongo) DeleteByAgeLessThan(ctx context.Context, arg0 int) (int, error) { + result, err := r.collection.DeleteMany(ctx, bson.M{ + "age": bson.M{"$lt": arg0}, + }) + if err != nil { + return 0, err + } + return int(result.DeletedCount), nil +} +`, + }, + { + Name: "delete with LessThanEqual comparator", + MethodSpec: spec.MethodSpec{ + Name: "DeleteByAgeLessThanEqual", + Params: []code.Param{ + {Name: "ctx", Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Name: "age", Type: code.SimpleType("int")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + Operation: spec.DeleteOperation{ + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{ + Predicates: []spec.Predicate{ + {Comparator: spec.ComparatorLessThanEqual, Field: "Age"}, + }, + }, + }, + }, + ExpectedCode: ` +func (r *UserRepositoryMongo) DeleteByAgeLessThanEqual(ctx context.Context, arg0 int) (int, error) { + result, err := r.collection.DeleteMany(ctx, bson.M{ + "age": bson.M{"$lte": arg0}, + }) + if err != nil { + return 0, err + } + return int(result.DeletedCount), nil +} +`, + }, + { + Name: "delete with GreaterThan comparator", + MethodSpec: spec.MethodSpec{ + Name: "DeleteByAgeGreaterThan", + Params: []code.Param{ + {Name: "ctx", Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Name: "age", Type: code.SimpleType("int")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + Operation: spec.DeleteOperation{ + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{ + Predicates: []spec.Predicate{ + {Comparator: spec.ComparatorGreaterThan, Field: "Age"}, + }, + }, + }, + }, + ExpectedCode: ` +func (r *UserRepositoryMongo) DeleteByAgeGreaterThan(ctx context.Context, arg0 int) (int, error) { + result, err := r.collection.DeleteMany(ctx, bson.M{ + "age": bson.M{"$gt": arg0}, + }) + if err != nil { + return 0, err + } + return int(result.DeletedCount), nil +} +`, + }, + { + Name: "delete with GreaterThanEqual comparator", + MethodSpec: spec.MethodSpec{ + Name: "DeleteByAgeGreaterThanEqual", + Params: []code.Param{ + {Name: "ctx", Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Name: "age", Type: code.SimpleType("int")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + Operation: spec.DeleteOperation{ + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{ + Predicates: []spec.Predicate{ + {Comparator: spec.ComparatorGreaterThanEqual, Field: "Age"}, + }, + }, + }, + }, + ExpectedCode: ` +func (r *UserRepositoryMongo) DeleteByAgeGreaterThanEqual(ctx context.Context, arg0 int) (int, error) { + result, err := r.collection.DeleteMany(ctx, bson.M{ + "age": bson.M{"$gte": arg0}, + }) + if err != nil { + return 0, err + } + return int(result.DeletedCount), nil +} +`, + }, + { + Name: "delete with Between comparator", + MethodSpec: spec.MethodSpec{ + Name: "DeleteByAgeBetween", + Params: []code.Param{ + {Name: "ctx", Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Name: "fromAge", Type: code.SimpleType("int")}, + {Name: "toAge", Type: code.SimpleType("int")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + Operation: spec.DeleteOperation{ + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{ + Predicates: []spec.Predicate{ + {Comparator: spec.ComparatorBetween, Field: "Age"}, + }, + }, + }, + }, + ExpectedCode: ` +func (r *UserRepositoryMongo) DeleteByAgeBetween(ctx context.Context, arg0 int, arg1 int) (int, error) { + result, err := r.collection.DeleteMany(ctx, bson.M{ + "age": bson.M{"$gte": arg0, "$lte": arg1}, + }) + if err != nil { + return 0, err + } + return int(result.DeletedCount), nil +} +`, + }, + { + Name: "delete with In comparator", + MethodSpec: spec.MethodSpec{ + Name: "DeleteByGenderIn", + Params: []code.Param{ + {Name: "ctx", Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Name: "gender", Type: code.ArrayType{ContainedType: code.SimpleType("Gender")}}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + Operation: spec.DeleteOperation{ + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{ + Predicates: []spec.Predicate{ + {Comparator: spec.ComparatorIn, Field: "Gender"}, + }, + }, + }, + }, + ExpectedCode: ` +func (r *UserRepositoryMongo) DeleteByGenderIn(ctx context.Context, arg0 []Gender) (int, error) { + result, err := r.collection.DeleteMany(ctx, bson.M{ + "gender": bson.M{"$in": arg0}, + }) + if err != nil { + return 0, err + } + return int(result.DeletedCount), nil +} +`, + }, + } + + for _, testCase := range testTable { + t.Run(testCase.Name, func(t *testing.T) { + generator := mongo.NewGenerator(userModel, "UserRepository") + buffer := new(bytes.Buffer) + + err := generator.GenerateMethod(testCase.MethodSpec, buffer) + + if err != nil { + t.Error(err) + } + if err := testutils.ExpectMultiLineString(testCase.ExpectedCode, buffer.String()); err != nil { + t.Error(err) + } + }) + } +} + +type GenerateMethodInvalidTestCase struct { + Name string + Method spec.MethodSpec + ExpectedError error +} + +func TestGenerateMethod_Invalid(t *testing.T) { + testTable := []GenerateMethodInvalidTestCase{ + { + Name: "operation not supported", + Method: spec.MethodSpec{ + Name: "SearchByID", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.ExternalType{PackageAlias: "primitive", Name: "ObjectID"}}, + }, + Returns: []code.Type{ + code.ArrayType{ContainedType: code.PointerType{ContainedType: code.SimpleType("UserModel")}}, + code.SimpleType("error"), + }, + Operation: "search", + }, + ExpectedError: mongo.OperationNotSupportedError, + }, + { + Name: "bson tag not found", + Method: spec.MethodSpec{ + Name: "FindByAccessToken", + 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.QueryModeOne, + Query: spec.QuerySpec{ + Predicates: []spec.Predicate{ + {Field: "AccessToken", Comparator: spec.ComparatorEqual}, + }, + }, + }, + }, + ExpectedError: mongo.BsonTagNotFoundError, + }, + } + + for _, testCase := range testTable { + t.Run(testCase.Name, func(t *testing.T) { + generator := mongo.NewGenerator(userModel, "UserRepository") + buffer := new(bytes.Buffer) + + err := generator.GenerateMethod(testCase.Method, buffer) + + if err != testCase.ExpectedError { + t.Errorf("\nExpected = %v\nReceived = %v", testCase.ExpectedError, err) + } + }) + } +} diff --git a/internal/mongo/templates.go b/internal/mongo/templates.go index 3e87a12..d5fb963 100644 --- a/internal/mongo/templates.go +++ b/internal/mongo/templates.go @@ -70,19 +70,19 @@ func (data mongoMethodTemplateData) Returns() string { return fmt.Sprintf(" (%s)", strings.Join(returns, ", ")) } -const findOneTemplate = ` var entity {{.EntityType}} - if err := r.collection.FindOne(ctx, bson.M{ -{{range $index, $field := .QuerySpec.Predicates}} {{$field.Code $index}}, -{{end}} }).Decode(&entity); err != nil { - return nil, err - } - return &entity, nil` - type mongoFindTemplateData struct { EntityType string QuerySpec querySpec } +const findOneTemplate = ` var entity {{.EntityType}} + if err := r.collection.FindOne(ctx, bson.M{ +{{.QuerySpec.Code}} + }).Decode(&entity); err != nil { + return nil, err + } + return &entity, nil` + const findManyTemplate = ` cursor, err := r.collection.Find(ctx, bson.M{ {{.QuerySpec.Code}} }) @@ -94,3 +94,23 @@ const findManyTemplate = ` cursor, err := r.collection.Find(ctx, bson.M{ return nil, err } return entities, nil` + +type mongoDeleteTemplateData struct { + QuerySpec querySpec +} + +const deleteOneTemplate = ` result, err := r.collection.DeleteOne(ctx, bson.M{ +{{.QuerySpec.Code}} + }) + if err != nil { + return false, err + } + return result.DeletedCount > 0, nil` + +const deleteManyTemplate = ` result, err := r.collection.DeleteMany(ctx, bson.M{ +{{.QuerySpec.Code}} + }) + if err != nil { + return 0, err + } + return int(result.DeletedCount), nil` diff --git a/internal/spec/models.go b/internal/spec/models.go index a993f6b..66def43 100644 --- a/internal/spec/models.go +++ b/internal/spec/models.go @@ -33,6 +33,12 @@ type FindOperation struct { Query QuerySpec } +// DeleteOperation is a method specification for delete operations +type DeleteOperation struct { + Mode QueryMode + Query QuerySpec +} + // QuerySpec is a set of conditions of querying the database type QuerySpec struct { Operator Operator diff --git a/internal/spec/parser.go b/internal/spec/parser.go index 95e91de..a069e81 100644 --- a/internal/spec/parser.go +++ b/internal/spec/parser.go @@ -25,6 +25,8 @@ func (p interfaceMethodParser) Parse() (MethodSpec, error) { switch methodNameTokens[0] { case "Find": return p.parseFindMethod(methodNameTokens[1:]) + case "Delete": + return p.parseDeleteMethod(methodNameTokens[1:]) } return MethodSpec{}, UnknownOperationError } @@ -92,6 +94,58 @@ func (p interfaceMethodParser) extractFindReturns(returns []code.Type) (QueryMod return "", UnsupportedReturnError } +func (p interfaceMethodParser) parseDeleteMethod(tokens []string) (MethodSpec, error) { + if len(tokens) == 0 { + return MethodSpec{}, UnsupportedNameError + } + + mode, err := p.extractDeleteReturns(p.Method.Returns) + if err != nil { + return MethodSpec{}, err + } + + querySpec, err := p.parseQuery(tokens) + if err != nil { + return MethodSpec{}, err + } + + if err := p.validateMethodSignature(querySpec); err != nil { + return MethodSpec{}, err + } + + return MethodSpec{ + Name: p.Method.Name, + Params: p.Method.Params, + Returns: p.Method.Returns, + Operation: DeleteOperation{ + Mode: mode, + Query: querySpec, + }, + }, nil +} + +func (p interfaceMethodParser) extractDeleteReturns(returns []code.Type) (QueryMode, error) { + if len(returns) != 2 { + return "", UnsupportedReturnError + } + + if returns[1] != code.SimpleType("error") { + return "", UnsupportedReturnError + } + + simpleType, ok := returns[0].(code.SimpleType) + if ok { + if simpleType == code.SimpleType("bool") { + return QueryModeOne, nil + } + if simpleType == code.SimpleType("int") { + return QueryModeMany, nil + } + } + + return "", UnsupportedReturnError +} + func (p interfaceMethodParser) parseQuery(tokens []string) (QuerySpec, error) { if len(tokens) == 0 { return QuerySpec{}, InvalidQueryError diff --git a/internal/spec/parser_test.go b/internal/spec/parser_test.go index feffacd..09b6230 100644 --- a/internal/spec/parser_test.go +++ b/internal/spec/parser_test.go @@ -40,7 +40,7 @@ type ParseInterfaceMethodTestCase struct { ExpectedOutput spec.MethodSpec } -func TestParseInterfaceMethod(t *testing.T) { +func TestParseInterfaceMethod_Find(t *testing.T) { testTable := []ParseInterfaceMethodTestCase{ { Name: "FindOneByArg method", @@ -470,21 +470,454 @@ func TestParseInterfaceMethod(t *testing.T) { } } +func TestParseInterfaceMethod_Delete(t *testing.T) { + testTable := []ParseInterfaceMethodTestCase{ + { + Name: "DeleteOneByArg method", + Method: code.Method{ + Name: "DeleteOneByID", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.ExternalType{PackageAlias: "primitive", Name: "ObjectID"}}, + }, + Returns: []code.Type{ + code.SimpleType("bool"), + code.SimpleType("error"), + }, + }, + ExpectedOutput: spec.MethodSpec{ + Name: "DeleteOneByID", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.ExternalType{PackageAlias: "primitive", Name: "ObjectID"}}, + }, + Returns: []code.Type{ + code.SimpleType("bool"), + code.SimpleType("error"), + }, + Operation: spec.DeleteOperation{ + Mode: spec.QueryModeOne, + Query: spec.QuerySpec{Predicates: []spec.Predicate{ + {Field: "ID", Comparator: spec.ComparatorEqual}, + }}, + }, + }, + }, + { + Name: "DeleteOneByMultiWordArg method", + Method: code.Method{ + Name: "DeleteOneByPhoneNumber", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + }, + Returns: []code.Type{ + code.SimpleType("bool"), + code.SimpleType("error"), + }, + }, + ExpectedOutput: spec.MethodSpec{ + Name: "DeleteOneByPhoneNumber", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + }, + Returns: []code.Type{ + code.SimpleType("bool"), + code.SimpleType("error"), + }, + Operation: spec.DeleteOperation{ + Mode: spec.QueryModeOne, + Query: spec.QuerySpec{Predicates: []spec.Predicate{ + {Field: "PhoneNumber", Comparator: spec.ComparatorEqual}, + }}, + }, + }, + }, + { + Name: "DeleteByArg method", + Method: code.Method{ + Name: "DeleteByCity", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + }, + ExpectedOutput: spec.MethodSpec{ + Name: "DeleteByCity", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + Operation: spec.DeleteOperation{ + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{Predicates: []spec.Predicate{ + {Field: "City", Comparator: spec.ComparatorEqual}, + }}, + }, + }, + }, + { + Name: "DeleteAll method", + Method: code.Method{ + Name: "DeleteAll", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + }, + ExpectedOutput: spec.MethodSpec{ + Name: "DeleteAll", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + Operation: spec.DeleteOperation{ + Mode: spec.QueryModeMany, + }, + }, + }, + { + Name: "DeleteByArgAndArg method", + Method: code.Method{ + Name: "DeleteByCityAndGender", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + {Type: code.SimpleType("Gender")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + }, + ExpectedOutput: spec.MethodSpec{ + Name: "DeleteByCityAndGender", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + {Type: code.SimpleType("Gender")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + Operation: spec.DeleteOperation{ + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{ + Operator: spec.OperatorAnd, + Predicates: []spec.Predicate{ + {Field: "City", Comparator: spec.ComparatorEqual}, + {Field: "Gender", Comparator: spec.ComparatorEqual}, + }, + }, + }, + }, + }, + { + Name: "DeleteByArgOrArg method", + Method: code.Method{ + Name: "DeleteByCityOrGender", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + {Type: code.SimpleType("Gender")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + }, + ExpectedOutput: spec.MethodSpec{ + Name: "DeleteByCityOrGender", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + {Type: code.SimpleType("Gender")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + Operation: spec.DeleteOperation{ + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{ + Operator: spec.OperatorOr, + Predicates: []spec.Predicate{ + {Field: "City", Comparator: spec.ComparatorEqual}, + {Field: "Gender", Comparator: spec.ComparatorEqual}, + }, + }, + }, + }, + }, + { + Name: "DeleteByArgNot method", + Method: code.Method{ + Name: "DeleteByCityNot", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + }, + ExpectedOutput: spec.MethodSpec{ + Name: "DeleteByCityNot", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + Operation: spec.DeleteOperation{ + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{Predicates: []spec.Predicate{ + {Field: "City", Comparator: spec.ComparatorNot}, + }}, + }, + }, + }, + { + Name: "DeleteByArgLessThan method", + Method: code.Method{ + Name: "DeleteByAgeLessThan", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("int")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + }, + ExpectedOutput: spec.MethodSpec{ + Name: "DeleteByAgeLessThan", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("int")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + Operation: spec.DeleteOperation{ + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{Predicates: []spec.Predicate{ + {Field: "Age", Comparator: spec.ComparatorLessThan}, + }}, + }, + }, + }, + { + Name: "DeleteByArgLessThanEqual method", + Method: code.Method{ + Name: "DeleteByAgeLessThanEqual", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("int")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + }, + ExpectedOutput: spec.MethodSpec{ + Name: "DeleteByAgeLessThanEqual", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("int")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + Operation: spec.DeleteOperation{ + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{Predicates: []spec.Predicate{ + {Field: "Age", Comparator: spec.ComparatorLessThanEqual}, + }}, + }, + }, + }, + { + Name: "DeleteByArgGreaterThan method", + Method: code.Method{ + Name: "DeleteByAgeGreaterThan", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("int")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + }, + ExpectedOutput: spec.MethodSpec{ + Name: "DeleteByAgeGreaterThan", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("int")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + Operation: spec.DeleteOperation{ + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{Predicates: []spec.Predicate{ + {Field: "Age", Comparator: spec.ComparatorGreaterThan}, + }}, + }, + }, + }, + { + Name: "DeleteByArgGreaterThanEqual method", + Method: code.Method{ + Name: "DeleteByAgeGreaterThanEqual", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("int")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + }, + ExpectedOutput: spec.MethodSpec{ + Name: "DeleteByAgeGreaterThanEqual", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("int")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + Operation: spec.DeleteOperation{ + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{Predicates: []spec.Predicate{ + {Field: "Age", Comparator: spec.ComparatorGreaterThanEqual}, + }}, + }, + }, + }, + { + Name: "DeleteByArgBetween method", + Method: code.Method{ + Name: "DeleteByAgeBetween", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("int")}, + {Type: code.SimpleType("int")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + }, + ExpectedOutput: spec.MethodSpec{ + Name: "DeleteByAgeBetween", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("int")}, + {Type: code.SimpleType("int")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + Operation: spec.DeleteOperation{ + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{Predicates: []spec.Predicate{ + {Field: "Age", Comparator: spec.ComparatorBetween}, + }}, + }, + }, + }, + { + Name: "DeleteByArgIn method", + Method: code.Method{ + Name: "DeleteByCityIn", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.ArrayType{ContainedType: code.SimpleType("string")}}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + }, + ExpectedOutput: spec.MethodSpec{ + Name: "DeleteByCityIn", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.ArrayType{ContainedType: code.SimpleType("string")}}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + Operation: spec.DeleteOperation{ + Mode: spec.QueryModeMany, + Query: spec.QuerySpec{Predicates: []spec.Predicate{ + {Field: "City", Comparator: spec.ComparatorIn}, + }}, + }, + }, + }, + } + + for _, testCase := range testTable { + t.Run(testCase.Name, func(t *testing.T) { + actualSpec, err := spec.ParseInterfaceMethod(structModel, testCase.Method) + + if err != nil { + t.Errorf("Error = %s", err) + } + if !reflect.DeepEqual(actualSpec, testCase.ExpectedOutput) { + t.Errorf("Expected = %v\nReceived = %v", testCase.ExpectedOutput, actualSpec) + } + }) + } +} + type ParseInterfaceMethodInvalidTestCase struct { Name string Method code.Method ExpectedError error } -func TestParseInterfaceMethodInvalid(t *testing.T) { +func TestParseInterfaceMethod_Invalid(t *testing.T) { + _, err := spec.ParseInterfaceMethod(structModel, code.Method{ + Name: "SearchByID", + }) + + if err != spec.UnknownOperationError { + t.Errorf("\nExpected = %v\nReceived = %v", spec.UnknownOperationError, err) + } +} + +func TestParseInterfaceMethod_Find_Invalid(t *testing.T) { testTable := []ParseInterfaceMethodInvalidTestCase{ - { - Name: "unknown operation", - Method: code.Method{ - Name: "SearchByID", - }, - ExpectedError: spec.UnknownOperationError, - }, { Name: "unsupported find method name", Method: code.Method{ @@ -657,3 +1090,178 @@ func TestParseInterfaceMethodInvalid(t *testing.T) { }) } } + +func TestParseInterfaceMethod_Delete_Invalid(t *testing.T) { + testTable := []ParseInterfaceMethodInvalidTestCase{ + { + Name: "unsupported delete method name", + Method: code.Method{ + Name: "Delete", + }, + ExpectedError: spec.UnsupportedNameError, + }, + { + Name: "invalid number of returns", + Method: code.Method{ + Name: "DeleteOneByID", + 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: "DeleteOneByID", + Returns: []code.Type{ + code.SimpleType("float64"), + code.SimpleType("error"), + }, + }, + ExpectedError: spec.UnsupportedReturnError, + }, + { + Name: "error return not provided", + Method: code.Method{ + Name: "DeleteOneByID", + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("bool"), + }, + }, + ExpectedError: spec.UnsupportedReturnError, + }, + { + Name: "misplaced operator token (leftmost)", + Method: code.Method{ + Name: "DeleteByAndGender", + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + }, + ExpectedError: spec.InvalidQueryError, + }, + { + Name: "misplaced operator token (rightmost)", + Method: code.Method{ + Name: "DeleteByGenderAnd", + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + }, + ExpectedError: spec.InvalidQueryError, + }, + { + Name: "misplaced operator token (double operator)", + Method: code.Method{ + Name: "DeleteByGenderAndAndCity", + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + }, + ExpectedError: spec.InvalidQueryError, + }, + { + Name: "ambiguous query", + Method: code.Method{ + Name: "DeleteByGenderAndCityOrAge", + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + }, + ExpectedError: spec.InvalidQueryError, + }, + { + Name: "no context parameter", + Method: code.Method{ + Name: "DeleteByGender", + Params: []code.Param{ + {Type: code.SimpleType("Gender")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + }, + ExpectedError: spec.ContextParamRequiredError, + }, + { + Name: "mismatched number of parameters", + Method: code.Method{ + Name: "DeleteByCountry", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + {Type: code.SimpleType("string")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + }, + ExpectedError: spec.InvalidParamError, + }, + { + Name: "struct field not found", + Method: code.Method{ + Name: "DeleteByCountry", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + }, + ExpectedError: spec.StructFieldNotFoundError, + }, + { + Name: "mismatched method parameter type", + Method: code.Method{ + Name: "DeleteByGender", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + code.SimpleType("error"), + }, + }, + ExpectedError: spec.InvalidParamError, + }, + { + Name: "mismatched method parameter type for special case", + Method: code.Method{ + Name: "DeleteByCityIn", + Params: []code.Param{ + {Type: code.ExternalType{PackageAlias: "context", Name: "Context"}}, + {Type: code.SimpleType("string")}, + }, + Returns: []code.Type{ + code.SimpleType("int"), + 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) + } + }) + } +}