Skip to content

Commit

Permalink
ZRouter: A Comprehensive Chi-based Routing Library (#18)
Browse files Browse the repository at this point in the history
* Add zrouter package

* Add groups

* Add error middleware

* Add rate limit

* Add rate limit

* Router tests

* Fix register metrics and middlewares

* Fix linter

* Add Readme

* Minor fix on error handler

* Update readme

* Update readme

* Minor change on defaultServiceResponse

* Fix zdb tests

* Add configurable logger

* Update router

* Add check tests in ci

* Update readme

* Update readme

* Validate appName

* Add lock to active connection metrics
  • Loading branch information
lucaslopezf authored Oct 25, 2023
1 parent cb06ff5 commit cad41f0
Show file tree
Hide file tree
Showing 24 changed files with 1,243 additions and 2 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/checks.golang.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ jobs:
export PATH=$PATH:$(go env GOPATH)/bin
make install_lint
make lint
- name: Run tests
run: |
make test
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ lint:
golangci-lint --version
golangci-lint run

test:
go test -v -race ./...

earthly:
earthly +all

Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ go 1.19
require (
github.com/ClickHouse/clickhouse-go/v2 v2.14.2
github.com/go-chi/chi/v5 v5.0.10
github.com/go-chi/cors v1.2.1
github.com/prometheus/client_golang v1.17.0
github.com/spf13/cobra v1.7.0
github.com/spf13/viper v1.17.0
github.com/stretchr/testify v1.8.4
go.uber.org/zap v1.26.0
golang.org/x/sync v0.4.0
golang.org/x/time v0.3.0
gorm.io/driver/clickhouse v0.5.1
gorm.io/driver/postgres v1.5.3
gorm.io/gorm v1.25.5
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,8 @@ github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2H
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
github.com/go-faster/errors v0.6.1 h1:nNIPOBkprlKzkThvS/0YaX8Zs9KewLCOSFQS5BU06FI=
Expand Down Expand Up @@ -1349,6 +1351,8 @@ golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
Expand Down
4 changes: 2 additions & 2 deletions pkg/zdb/methods_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,14 @@ func (suite *ZDatabaseSuite) TestExec() {

func (suite *ZDatabaseSuite) TestSelect() {
suite.db.(*MockZDatabase).On("Select", "name", []interface{}{"Messi"}).Return(suite.db)
newDb := suite.db.Select("name", "Messi")
newDb := suite.db.Select("name", []interface{}{"Messi"})
suite.NotNil(newDb)
suite.db.(*MockZDatabase).AssertExpectations(suite.T())
}

func (suite *ZDatabaseSuite) TestWhere() {
suite.db.(*MockZDatabase).On("Where", "name = ?", []interface{}{"Messi"}).Return(suite.db)
newDb := suite.db.Where("name = ?", "Messi")
newDb := suite.db.Where("name = ?", []interface{}{"Messi"})
suite.NotNil(newDb)
suite.db.(*MockZDatabase).AssertExpectations(suite.T())
}
Expand Down
240 changes: 240 additions & 0 deletions pkg/zrouter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
# ZRouter package

ZRouter is a Golang routing library built on the robust foundation of the chi router.

## Table of Contents

- [Features](#features)
- [Getting Started](#getting-started)
- [Installation](#installation)
- [Usage](#usage)
- [Routing](#routing)
- [Middleware](#middleware)
- [Adapters](#adapters)
- [Custom Configurations](#custom-configurations)
- [Monitoring and Logging](#monitoring-and-logging)
- [Advanced Topics](#advanced-topics)
- [Examples](#examples)
- [Conclusion](#conclusion)

## Features

- **Intuitive Routing Interface**: Define your routes with ease using all major HTTP methods.
- **Middleware Chaining**: Introduce layers of middleware to your HTTP requests and responses.
- **Flexible Adapters**: Seamlessly integrate with the `chi` router's context.
- **Enhanced Monitoring**: Integrated metrics server and structured logging for in-depth observability.
- **Customizable Settings**: Adjust server configurations like timeouts to suit your needs.

## Getting Started

### Installation

To incorporate ZRouter into your project:

```bash
go get github.com/zondax/golem/pkg/zrouter
```

## Usage

Crafting a web service using ZRouter:

```go
import "github.com/zondax/golem/pkg/zrouter"

config := &zrouter.Config{ReadTimeOut: 10 * time.Second, WriteTimeOut: 10 * time.Second}
router := zrouter.New("ServiceName", metricServer, config)

router.Use(middlewareLogic)

groupedRoutes := router.Group("/grouped")
groupedRoutes.GET("/{param}", handlerFunction)
```

or

```go
import "github.com/zondax/golem/pkg/zrouter"

func main() {
router := zrouter.New("MyService", metricServer, nil)

router.GET("/endpoint", func(ctx zrouter.Context) (domain.ServiceResponse, error) {
// Handler implementation
})

router.Run()
}
```

## Routing

For dynamic URL parts, utilize the chi style, e.g., /entities/{entityID}.

## Middleware

Add pre- and post-processing steps to your routes. Chain multiple middlewares for enhanced functionality.

### **Default Middlewares**

`ZRouter` comes bundled with certain default middlewares for enhanced functionality and ease of use:

- **ErrorHandlerMiddleware**: Systematically manages errors by translating them into a consistent response format.
- **RequestID()**: Attaches a unique request ID to every request, facilitating request tracking and debugging.
- **RequestMetrics()**: Monitors and logs metrics associated with requests, responses, and other interactions for performance insights.

To activate these middlewares, make sure to call:

```go
zr := zrouter.New("AppName", metricsServer, nil)
zr.SetDefaultMiddlewares() //Call this method!
```
### **Additional Middlewares**

Beyond the default offerings, `ZRouter` also provides extra middlewares to address specific needs:

- **DefaultCors()**: Introduces a predefined set of Cross-Origin Resource Sharing (CORS) rules, facilitating browsers to make requests across origins safely.
- **Cors(options CorsOptions)**: A flexible CORS middleware that allows you to set specific CORS policies, such as permitted origins, headers, and methods, tailored to your application's demands.
- **RateLimit(maxRPM int)**: Shields your application from being swamped by imposing a rate limit on the influx of requests. By setting `maxRPM`, you can decide the maximum number of permissible requests per minute.

## Adapters

Use `chiContextAdapter` for translating the `chi` router's context to ZRouter's.

## Custom Configurations

Specify server behavior with `Config`. Use default settings or customize as needed.

Default settings:
- `ReadTimeOut`: 240000 milliseconds.
- `WriteTimeOut`: 240000 milliseconds.
- `Logger`: Uses production logger settings by default.

Override these defaults by providing values during initialization.

Example:
```go
config := &Config{
ReadTimeOut: 25000 * time.Millisecond,
WriteTimeOut: 25000 * time.Millisecond,
Logger: zapLoggerInstance,
}
zr := New("YourAppName", metricsServerInstance, config)
```

## Response Standards

### ServiceResponse

When handling responses, ZRouter provides a standardized way to return them using `ServiceResponse`, which includes status, headers, and body.

**Example**:

```go
func MyHandler(ctx Context) (domain.ServiceResponse, error) {
data := map[string]string{"message": "Hello, World!"}
return domain.NewServiceResponse(http.StatusOK, data), nil
}
```

### Handling Headers

With `ServiceResponse`, you can easily set custom headers for your responses:

```go
func MyHandler(ctx Context) (domain.ServiceResponse, error) {
headers := make(http.Header)
headers.Set("X-Custom-Header", "My Value")

data := map[string]string{"message": "Hello, World!"}
response := domain.NewServiceResponseWithHeader(http.StatusOK, data, headers)
return response, nil
}
```
### Error Handling

Whenever you return an error, ZRouter translates it to a structured error response, maintaining consistency across your services.

**Example**:

```go
func MyHandler(ctx Context) (domain.ServiceResponse, error) {
return nil, domain.NewAPIErrorResponse(http.StatusNotFound, "not_found", "message")
}
```

## Context in ZRouter

The `Context` is an essential part of ZRouter, providing a consistent interface to interact with the HTTP request and offering helper methods to streamline handler operations. This abstraction ensures that, as your router's needs evolve, the core interface to access request information remains consistent.

### Functions and Usage:

1. **Request**:

Retrieve the raw `*http.Request` from the context:

```go
req := ctx.Request()
```

2. **BindJSON**:

Decode a JSON request body directly into a provided object:

```go
var myData MyStruct
err := ctx.BindJSON(&myData)
```

3. **Header**:

Set an HTTP header for the response:

```go
ctx.Header("X-Custom-Header", "Custom Value")
```

4. **Param**:

Get URL parameters (path variables):

```go
userID := ctx.Param("userID")
```

5. **Query**:

Retrieve a query parameter from the URL:

```go
sortBy := ctx.Query("sortBy")
```

6. **DefaultQuery**:

Retrieve a query parameter from the URL, but return a default value if it's not present:
```go
order := ctx.DefaultQuery("order", "asc")
```
### Adapting to chi:
Behind the scenes, ZRouter leverages the powerful `chi` router. The `chiContextAdapter` translates the chi context to ZRouter's, ensuring that you get the benefits of chi's speed and power with ZRouter's simplified and consistent interface.

## Monitoring and Logging

Monitor request metrics and employ structured logging for in-depth insights.

## Advanced Topics

- **Route Grouping**: Consolidate routes under specific prefixes using `Group()`.
- **NotFound Handling**: Specify custom logic for unmatched routes.
- **Route Tracking**: Fetch a structured list of all registered routes.

### **Why ZRouter?**

- **Consistent Standard:** In a world full of routers, `ZRouter` gives us a way to keep things standard across our projects.
- **Flexibility:** Today we're using `chi`, but what about tomorrow? With `ZRouter`, if we ever want to switch, we can do it here and keep everything else unchanged.
- **Speed & Power of Chi:** We get all the speed and flexibility of routers like `chi` but without tying ourselves down to one specific router.
- **Unified Approach:** `ZRouter` sets a clear standard for how we handle metrics, responses, errors, and more. It's about making sure everything works the same way, every time.
50 changes: 50 additions & 0 deletions pkg/zrouter/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package zrouter

import (
"encoding/json"
"github.com/go-chi/chi/v5"
"net/http"
)

type Context interface {
Request() *http.Request
BindJSON(obj interface{}) error
Header(key, value string)
Param(key string) string
Query(key string) string
DefaultQuery(key, defaultValue string) string
}

type chiContextAdapter struct {
ctx http.ResponseWriter
req *http.Request
}

func (c *chiContextAdapter) Request() *http.Request {
return c.req
}

func (c *chiContextAdapter) BindJSON(obj interface{}) error {
return json.NewDecoder(c.req.Body).Decode(obj)
}

func (c *chiContextAdapter) Header(key, value string) {
c.ctx.Header().Set(key, value)
}

func (c *chiContextAdapter) Param(key string) string {
return chi.URLParam(c.req, key)
}

func (c *chiContextAdapter) Query(key string) string {
values := c.req.URL.Query()
return values.Get(key)
}

func (c *chiContextAdapter) DefaultQuery(key, defaultValue string) string {
value := c.Query(key)
if value == "" {
return defaultValue
}
return value
}
Loading

0 comments on commit cad41f0

Please sign in to comment.