Add delete operation
This commit is contained in:
parent
c5c4542a80
commit
e78f9180da
8 changed files with 1278 additions and 88 deletions
|
@ -3,7 +3,7 @@ coverage:
|
||||||
project:
|
project:
|
||||||
default:
|
default:
|
||||||
target: 75%
|
target: 75%
|
||||||
threshold: 5%
|
threshold: 4%
|
||||||
patch:
|
patch:
|
||||||
default:
|
default:
|
||||||
target: 50%
|
target: 50%
|
||||||
|
|
20
internal/mongo/errors.go
Normal file
20
internal/mongo/errors.go
Normal file
|
@ -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"
|
||||||
|
)
|
|
@ -2,7 +2,6 @@ package mongo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
@ -80,35 +79,24 @@ func (g RepositoryGenerator) generateMethodImplementation(methodSpec spec.Method
|
||||||
switch operation := methodSpec.Operation.(type) {
|
switch operation := methodSpec.Operation.(type) {
|
||||||
case spec.FindOperation:
|
case spec.FindOperation:
|
||||||
return g.generateFindImplementation(operation)
|
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) {
|
func (g RepositoryGenerator) generateFindImplementation(operation spec.FindOperation) (string, error) {
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(bytes.Buffer)
|
||||||
|
|
||||||
var predicates []predicate
|
querySpec, err := g.mongoQuerySpec(operation.Query)
|
||||||
for _, predicateSpec := range operation.Query.Predicates {
|
if err != nil {
|
||||||
structField, ok := g.StructModel.Fields.ByName(predicateSpec.Field)
|
return "", err
|
||||||
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})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tmplData := mongoFindTemplateData{
|
tmplData := mongoFindTemplateData{
|
||||||
EntityType: g.StructModel.Name,
|
EntityType: g.StructModel.Name,
|
||||||
QuerySpec: querySpec{
|
QuerySpec: querySpec,
|
||||||
Operator: operation.Query.Operator,
|
|
||||||
Predicates: predicates,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if operation.Mode == spec.QueryModeOne {
|
if operation.Mode == spec.QueryModeOne {
|
||||||
|
@ -134,6 +122,64 @@ func (g RepositoryGenerator) generateFindImplementation(operation spec.FindOpera
|
||||||
return buffer.String(), nil
|
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 {
|
func (g RepositoryGenerator) structName() string {
|
||||||
return g.InterfaceName + "Mongo"
|
return g.InterfaceName + "Mongo"
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,28 +10,7 @@ import (
|
||||||
"github.com/sunboyy/repogen/internal/testutils"
|
"github.com/sunboyy/repogen/internal/testutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const expectedConstructorResult = `
|
var userModel = code.Struct{
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"go.mongodb.org/mongo-driver/bson"
|
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
|
||||||
"go.mongodb.org/mongo-driver/mongo"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewUserRepository(collection *mongo.Collection) UserRepository {
|
|
||||||
return &UserRepositoryMongo{
|
|
||||||
collection: collection,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserRepositoryMongo struct {
|
|
||||||
collection *mongo.Collection
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
func TestGenerateConstructor(t *testing.T) {
|
|
||||||
userModel := code.Struct{
|
|
||||||
Name: "UserModel",
|
Name: "UserModel",
|
||||||
Fields: code.StructFields{
|
Fields: code.StructFields{
|
||||||
{
|
{
|
||||||
|
@ -54,8 +33,34 @@ func TestGenerateConstructor(t *testing.T) {
|
||||||
Type: code.SimpleType("int"),
|
Type: code.SimpleType("int"),
|
||||||
Tags: map[string][]string{"bson": {"age"}},
|
Tags: map[string][]string{"bson": {"age"}},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "AccessToken",
|
||||||
|
Type: code.SimpleType("string"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const expectedConstructorResult = `
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewUserRepository(collection *mongo.Collection) UserRepository {
|
||||||
|
return &UserRepositoryMongo{
|
||||||
|
collection: collection,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserRepositoryMongo struct {
|
||||||
|
collection *mongo.Collection
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestGenerateConstructor(t *testing.T) {
|
||||||
generator := mongo.NewGenerator(userModel, "UserRepository")
|
generator := mongo.NewGenerator(userModel, "UserRepository")
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(bytes.Buffer)
|
||||||
|
|
||||||
|
@ -75,7 +80,7 @@ type GenerateMethodTestCase struct {
|
||||||
ExpectedCode string
|
ExpectedCode string
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerateMethod(t *testing.T) {
|
func TestGenerateMethod_Find(t *testing.T) {
|
||||||
testTable := []GenerateMethodTestCase{
|
testTable := []GenerateMethodTestCase{
|
||||||
{
|
{
|
||||||
Name: "simple find one method",
|
Name: "simple find one method",
|
||||||
|
@ -489,33 +494,9 @@ func (r *UserRepositoryMongo) FindByGenderIn(ctx context.Context, arg0 []Gender)
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, testCase := range testTable {
|
for _, testCase := range testTable {
|
||||||
t.Run(testCase.Name, func(t *testing.T) {
|
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")
|
generator := mongo.NewGenerator(userModel, "UserRepository")
|
||||||
buffer := new(bytes.Buffer)
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -70,19 +70,19 @@ func (data mongoMethodTemplateData) Returns() string {
|
||||||
return fmt.Sprintf(" (%s)", strings.Join(returns, ", "))
|
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 {
|
type mongoFindTemplateData struct {
|
||||||
EntityType string
|
EntityType string
|
||||||
QuerySpec querySpec
|
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{
|
const findManyTemplate = ` cursor, err := r.collection.Find(ctx, bson.M{
|
||||||
{{.QuerySpec.Code}}
|
{{.QuerySpec.Code}}
|
||||||
})
|
})
|
||||||
|
@ -94,3 +94,23 @@ const findManyTemplate = ` cursor, err := r.collection.Find(ctx, bson.M{
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return entities, nil`
|
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`
|
||||||
|
|
|
@ -33,6 +33,12 @@ type FindOperation struct {
|
||||||
Query QuerySpec
|
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
|
// QuerySpec is a set of conditions of querying the database
|
||||||
type QuerySpec struct {
|
type QuerySpec struct {
|
||||||
Operator Operator
|
Operator Operator
|
||||||
|
|
|
@ -25,6 +25,8 @@ func (p interfaceMethodParser) Parse() (MethodSpec, error) {
|
||||||
switch methodNameTokens[0] {
|
switch methodNameTokens[0] {
|
||||||
case "Find":
|
case "Find":
|
||||||
return p.parseFindMethod(methodNameTokens[1:])
|
return p.parseFindMethod(methodNameTokens[1:])
|
||||||
|
case "Delete":
|
||||||
|
return p.parseDeleteMethod(methodNameTokens[1:])
|
||||||
}
|
}
|
||||||
return MethodSpec{}, UnknownOperationError
|
return MethodSpec{}, UnknownOperationError
|
||||||
}
|
}
|
||||||
|
@ -92,6 +94,58 @@ func (p interfaceMethodParser) extractFindReturns(returns []code.Type) (QueryMod
|
||||||
return "", UnsupportedReturnError
|
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) {
|
func (p interfaceMethodParser) parseQuery(tokens []string) (QuerySpec, error) {
|
||||||
if len(tokens) == 0 {
|
if len(tokens) == 0 {
|
||||||
return QuerySpec{}, InvalidQueryError
|
return QuerySpec{}, InvalidQueryError
|
||||||
|
|
|
@ -40,7 +40,7 @@ type ParseInterfaceMethodTestCase struct {
|
||||||
ExpectedOutput spec.MethodSpec
|
ExpectedOutput spec.MethodSpec
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseInterfaceMethod(t *testing.T) {
|
func TestParseInterfaceMethod_Find(t *testing.T) {
|
||||||
testTable := []ParseInterfaceMethodTestCase{
|
testTable := []ParseInterfaceMethodTestCase{
|
||||||
{
|
{
|
||||||
Name: "FindOneByArg method",
|
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 {
|
type ParseInterfaceMethodInvalidTestCase struct {
|
||||||
Name string
|
Name string
|
||||||
Method code.Method
|
Method code.Method
|
||||||
ExpectedError error
|
ExpectedError error
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseInterfaceMethodInvalid(t *testing.T) {
|
func TestParseInterfaceMethod_Invalid(t *testing.T) {
|
||||||
testTable := []ParseInterfaceMethodInvalidTestCase{
|
_, err := spec.ParseInterfaceMethod(structModel, code.Method{
|
||||||
{
|
|
||||||
Name: "unknown operation",
|
|
||||||
Method: code.Method{
|
|
||||||
Name: "SearchByID",
|
Name: "SearchByID",
|
||||||
},
|
})
|
||||||
ExpectedError: spec.UnknownOperationError,
|
|
||||||
},
|
if err != spec.UnknownOperationError {
|
||||||
|
t.Errorf("\nExpected = %v\nReceived = %v", spec.UnknownOperationError, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseInterfaceMethod_Find_Invalid(t *testing.T) {
|
||||||
|
testTable := []ParseInterfaceMethodInvalidTestCase{
|
||||||
{
|
{
|
||||||
Name: "unsupported find method name",
|
Name: "unsupported find method name",
|
||||||
Method: code.Method{
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue