Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Finalize v0.3 #38

Merged
merged 1 commit into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 37 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,35 +27,38 @@ This getting started tutorial shows a simple example on how to use repogen. You

### Step 1: Download and install repogen

Run this command in your terminal to download and install repogen
Run this command in your terminal to download and install the latest version of repogen

```
$ go get github.com/sunboyy/repogen
$ go install github.com/sunboyy/repogen@latest
```

### Step 2: Write a repository specification

Write repository specification as an interface in the same file as the model struct. There are 5 types of operations that are currently supported and are determined by the first word of the method name. Single-entity and multiple-entity modes are determined be the first return value. More complex queries can also be written.
Write repository specification as an interface in the same package as the model struct. There are 5 types of operations that are currently supported and are determined on the first word of the method name. Single-entity and multiple-entity modes are determined on the first return value. More complex queries can also be written.

```go
// You write this interface specification (comment is optional)
type UserRepository interface {
// InsertOne stores userModel into the database and returns inserted ID if insertion
// succeeds and returns error if insertion fails.
// InsertOne stores userModel into the database and returns inserted ID if
// insertion succeeds and returns error if insertion fails.
InsertOne(ctx context.Context, userModel *UserModel) (interface{}, error)

// FindByUsername queries user by username. If a user with specified username exists,
// the user will be returned. Otherwise, error will be returned.
// FindByUsername queries user by username. If a user with specified
// username exists, the user will be returned. Otherwise, error will be
// returned.
FindByUsername(ctx context.Context, username string) (*UserModel, error)

// UpdateDisplayNameByID updates a user with the specified ID with a new display name.
// If there is a user matches the query, it will return true. Error will be returned
// only when error occurs while accessing the database.
// UpdateDisplayNameByID updates a user with the specified ID with a new
// display name. If there is a user matches the query, it will return true.
// Error will be returned only when error occurs while accessing the
// database.
UpdateDisplayNameByID(ctx context.Context, displayName string, id primitive.ObjectID) (bool, error)

// DeleteByCity deletes users that have `city` value match the parameter and returns
// the match count. The error will be returned only when error occurs while accessing
// the database. This is a MANY mode because the first return type is an integer.
// DeleteByCity deletes users that have `city` value match the parameter
// and returns the match count. The error will be returned only when an
// error occurs while accessing the database. This is a MANY mode because
// the first return type is an integer.
DeleteByCity(ctx context.Context, city string) (int, error)

// CountByCity returns the number of rows that match the given city parameter. If an
Expand All @@ -69,22 +72,22 @@ type UserRepository interface {
Run the repogen to generate a repository implementation from the interface. The following command is an example to generate `UserRepository` interface implementation defined in `examples/getting-started/user.go` to the destination file `examples/getting-started/user_repo.go`. See [Usage](#Usage) section below for more detailed information.

```
$ repogen -src=examples/getting-started/user.go -dest=examples/getting-started/user_repo.go \
$ repogen -pkg=examples/getting-started -dest=examples/getting-started/user_repo.go \
-model=UserModel -repo=UserRepository
```

You can also write the above command in the `go:generate` format inside Go files in order to generate the implementation when `go generate` command is executed.
You can also write the above command in the `go:generate` format inside Go files in order to generate the implementation when `go generate` command is executed. If the command is written in the corresponding package, the `-pkg` flag can be ignored.

## Usage

### Running Options

The `repogen` command is used to generate source code for a given Go file containing repository interface to be implemented. Run `repogen -h` to see all available options while the necessary options for code generation are described as follows:
The `repogen` command is used to generate source code for a given Go package containing repository interface to be implemented. Run `repogen -h` to see all available options while the options for code generation are described as follows:

- `-src`: A Go file containing struct model and repository interface to be implemented
- `-dest`: A file to which to write the resulting source code. If not specified, the source code will be printed to the standard output.
- `-pkg`: A path to the package containing struct model and repository interface to be implemented. (Default: Current working directory)
- `-dest`: A path to the file to output the resulting source code. (Default: Print to standard output)
- `-model`: The name of the base struct model that represents the data stored in MongoDB for a specific collection.
- `-repo`: The name of the repository interface that you want to be implemented.
- `-repo`: The name of the repository interface that you want to be implemented according to the `-model` flag.

### Method Definition

Expand Down Expand Up @@ -146,6 +149,16 @@ FindByCityOrderByAgeAsc(ctx context.Context, city string) ([]*Model, error)
FindByCityOrderByAgeDesc(ctx context.Context, city string) ([]*Model, error)
```

If you want the result to be limited to the maximum of N items, you can also specify `TopN` immediately after the `Find` keyword.

```go
// This will return top 5 youngest users.
FindTop5AllOrderByAge(ctx context.Context) ([]*Model, error)

// This will return top 5 youngest users in the specified city.
FindTop5ByCityOrderByAge(ctx context.Context, city string) ([]*Model, error)
```

#### Update operation

An `Update` operation also has single-entity and multiple-entity operations. An `Update` operation also supports querying like `Find` operation. Specifying the query is the same as in `Find` method. However, an `Update` operation requires more parameters than `Find` method depending on update type. There are two update types provided.
Expand Down Expand Up @@ -247,10 +260,12 @@ When you specify the query like `ByAge`, it finds documents that contains age va
| `NotIn` | not in slice $1 | `FindByCityNotIn(ctx, $1)` |
| `True` | == `true` | `FindByEnabledTrue(ctx)` |
| `False` | == `false` | `FindByEnabledFalse(ctx)` |
| `Exists` | key exists | `FindByContactExists(ctx)` |
| `NotExists` | key not exists | `FindByContactNotExists(ctx)` |

To apply these comparators to the query, place the keyword after the field name such as `ByAgeGreaterThan`. You can also use comparators along with `And` and `Or` operators. For example, `ByGenderNotOrAgeLessThan` will apply `Not` comparator to the `Gender` field and `LessThan` comparator to the `Age` field.

`Between`, `In`, `NotIn`, `True` and `False` comparators are special in terms of parameter requirements. `Between` needs two parameters to perform the query, `In` and `NotIn` needs a slice instead of its raw type and `True` and `False` doesn't need any parameter. The example is provided below:
`Between`, `In`, `NotIn`, `True`, `False`, `Exists` and `NotExists` comparators are special in terms of parameter requirements. `Between` needs two parameters to perform the query, `In` and `NotIn` needs a slice instead of its raw type and `True`, `False`, `Exists` and `NotExists` doesn't need any parameter. The example is provided below:

```go
FindByAgeBetween(ctx context.Context, fromAge int, toAge int) ([]*UserModel, error)
Expand All @@ -260,6 +275,8 @@ FindByCityNotIn(ctx context.Context, cities []string) ([]*UserModel, error)

FindByEnabledTrue(ctx context.Context) ([]*UserModel, error)
FindByEnabledFalse(ctx context.Context) ([]*UserModel, error)
FindByContactExists(ctx context.Context) ([]*UserModel, error)
FindByContactNotExists(ctx context.Context) ([]*UserModel, error)
```

Assuming that the `Age` field in the `UserModel` struct is of type `int`, it requires that there must be two `int` parameters provided for `Age` field in the method. And assuming that the `City` field in the `UserModel` struct is of type `string`, it requires that the parameter that is provided to the query must be of slice type.
Expand Down
67 changes: 67 additions & 0 deletions examples/complex-query/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package main

import (
"context"
"fmt"

"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)

// Replace these values with your own connection option. This connection option is hard-coded for easy
// demonstration. Make sure not to hard-code the credentials in the production code.
const (
connectionString = "mongodb://admin:password@localhost:27017"
databaseName = "repogen_examples"
collectionName = "complexquery_user"
)

var (
userComparatorRepository UserComparatorRepository
userOtherRepository UserOtherRepository
)

func init() {
// create a connection to the database
client, err := mongo.NewClient(options.Client().ApplyURI(connectionString))
if err != nil {
panic(err)
}
err = client.Connect(context.TODO())
if err != nil {
panic(err)
}

// instantiate a user repository from the user collection
collection := client.Database(databaseName).Collection(collectionName)
userComparatorRepository = NewUserComparatorRepository(collection)
userOtherRepository = NewUserOtherRepository(collection)
}

func main() {
demonstrateFindByExists()
demonstrateFindByNotExists()
}

// demonstrateFindByExists shows how find method in repogen works. It receives
// query parameters through method arguments and returns matched result
func demonstrateFindByExists() {
users, err := userComparatorRepository.FindByContactExists(context.Background())
if err != nil {
panic(err)
}

fmt.Printf("FindByExists: found users = %+v\n", users)
}

// demonstrateFindByNotExists shows how find method in repogen works. It
// receives query parameters through method arguments and returns matched
// result.
func demonstrateFindByNotExists() {
users, err := userComparatorRepository.FindByContactNotExists(context.Background())
if err != nil {
panic(err)
}

fmt.Printf("FindByNotExists: found users = %+v\n", users)
}
66 changes: 66 additions & 0 deletions examples/complex-query/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package main

import (
"context"

"go.mongodb.org/mongo-driver/bson/primitive"
)

type Gender string

const (
GenderMale Gender = "MALE"
GenderFemale Gender = "FEMALE"
)

type UserModel struct {
ID primitive.ObjectID `bson:"_id,omitempty" json:"id"`
Username string `bson:"username" json:"username"`
Gender Gender `bson:"gender" json:"gender"`
Age int `bson:"age" json:"age"`
City string `bson:"city" json:"city"`
Contact *UserContactModel `bson:"contact,omitempty" json:"contact"`
Banned bool `bson:"banned" json:"banned"`
}

type UserContactModel struct {
Phone string `bson:"phone" json:"phone"`
Email string `bson:"email" json:"email"`
}

//go:generate repogen -dest=user_comparator_repo.go -model=UserModel -repo=UserComparatorRepository

// UserComparatorRepository is an interface that describes the specification of
// querying user data in the database.
type UserComparatorRepository interface {
FindByUsername(ctx context.Context, username string) (*UserModel, error)
FindByAgeGreaterThan(ctx context.Context, age int) ([]*UserModel, error)
FindByAgeGreaterThanEqual(ctx context.Context, age int) ([]*UserModel, error)
FindByAgeLessThan(ctx context.Context, age int) ([]*UserModel, error)
FindByAgeLessThanEqual(ctx context.Context, age int) ([]*UserModel, error)
FindByAgeBetween(ctx context.Context, fromAge int, toAge int) ([]*UserModel, error)
FindByCityNot(ctx context.Context, city string) ([]*UserModel, error)
FindByCityIn(ctx context.Context, cities []string) ([]*UserModel, error)
FindByCityNotIn(ctx context.Context, cities []string) ([]*UserModel, error)
FindByBannedTrue(ctx context.Context) ([]*UserModel, error)
FindByBannedFalse(ctx context.Context) ([]*UserModel, error)
FindByContactExists(ctx context.Context) (*UserModel, error)
FindByContactNotExists(ctx context.Context) (*UserModel, error)
}

//go:generate repogen -dest=user_other_repo.go -model=UserModel -repo=UserOtherRepository

type UserOtherRepository interface {
// FindByContactEmail demonstrates deeply-reference field (contect.email).
FindByContactEmail(ctx context.Context, email string) (*UserModel, error)

// FindByAgeAndCity demonstrates $and operation between two comparisons.
FindByAgeAndCity(ctx context.Context, age int, city string) ([]*UserModel, error)

// FindByGenderOrAgeGreaterThan demonstrates $or operation between two
// comparisons.
FindByGenderOrAgeGreaterThan(ctx context.Context, gender Gender, age int) ([]*UserModel, error)

// FindTop5AllOrderByAge demonstrates limiting find many results
FindTop5AllOrderByAge(ctx context.Context) ([]*UserModel, error)
}
Loading
Loading