GithubHelp home page GithubHelp logo

go-goyave / goyave Goto Github PK

View Code? Open in Web Editor NEW
1.4K 29.0 61.0 10.39 MB

🍐 The enterprise REST API framework

Home Page: https://goyave.dev

License: MIT License

JavaScript 0.01% Go 99.72% Shell 0.27% HTML 0.01%
go framework http api web-framework rest backend golang enterprise

goyave's Introduction

Goyave Logo Goyave Logo

Version Build Status Coverage Status Go Report

License Awesome Discord

An Elegant Golang Web Framework

Goyave is a progressive and accessible web application framework focused on REST APIs, aimed at making backend development easy and enjoyable. It has a philosophy of cleanliness and conciseness to make programs more elegant, easier to maintain and more focused. Goyave is an opinionated framework, helping your applications keeping a uniform architecture and limit redundancy. With Goyave, expect a full package with minimum setup.

  • Clean Code: Goyave has an expressive, elegant syntax, a robust structure and conventions. Minimalist calls and reduced redundancy are among the Goyave's core principles.
  • Fast Development: Develop faster and concentrate on the business logic of your application thanks to the many helpers and built-in functions.
  • Powerful functionalities: Goyave is accessible, yet powerful. The framework includes routing, request parsing, validation, localization, testing, authentication, and more!
  • Reliability: Error reporting is made easy thanks to advanced error handling and panic recovery. The framework is deeply tested.

Table of contents

Learning Goyave

The Goyave framework has an extensive documentation covering in-depth subjects and teaching you how to run a project using Goyave from setup to deployment.

Getting started

Requirements

  • Go 1.18+

Install using the template project

You can bootstrap your project using the Goyave template project. This project has a complete directory structure already set up for you.

Linux / MacOS

$ curl https://goyave.dev/install.sh | bash -s github.com/username/projectname

Windows (Powershell)

> & ([scriptblock]::Create((curl "https://goyave.dev/install.ps1").Content)) -moduleName github.com/username/projectname

Run go run . in your project's directory to start the server, then try to request the hello route.

$ curl http://localhost:8080/hello
Hi!

There is also an echo route, with basic validation of the request body.

$ curl -H "Content-Type: application/json" -X POST -d '{"text":"abc 123"}' http://localhost:8080/echo
abc 123

Features tour

This section's goal is to give a brief look at the main features of the framework. It doesn't describe everything the framework has to offer, so don't consider this documentation. If you want a complete reference and documentation, head to pkg.go.dev and the official documentation.

Hello world from scratch

The example below shows a basic Hello world application using Goyave.

import "goyave.dev/goyave/v4"

func registerRoutes(router *goyave.Router) {
    router.Get("/hello", func(response *goyave.Response, request *goyave.Request) {
        response.String(http.StatusOK, "Hello world!")
    })
}

func main() {
    if err := goyave.Start(registerRoutes); err != nil {
        os.Exit(err.(*goyave.Error).ExitCode)
    }
}

Configuration

To configure your application, use the config.json file at your project's root. If you are using the template project, copy config.example.json to config.json. The following code is an example of configuration for a local development environment:

{
    "app": {
        "name": "goyave_template",
        "environment": "localhost",
        "debug": true,
        "defaultLanguage": "en-US"
    },
    "server": {
        "host": "127.0.0.1",
        "maintenance": false,
        "protocol": "http",
        "domain": "",
        "port": 8080,
        "httpsPort": 8081,
        "timeout": 10,
        "maxUploadSize": 10
    },
    "database": {
        "connection": "mysql",
        "host": "127.0.0.1",
        "port": 3306,
        "name": "goyave",
        "username": "root",
        "password": "root",
        "options": "charset=utf8mb4&collation=utf8mb4_general_ci&parseTime=true&loc=Local",
        "maxOpenConnections": 20,
        "maxIdleConnections": 20,
        "maxLifetime": 300,
        "autoMigrate": false
    }
}

If this config file misses some config entries, the default values will be used.

All entries are validated. That means that the application will not start if you provided an invalid value in your config (for example if the specified port is not a number). That also means that a goroutine trying to change a config entry with the incorrect type will panic.
Entries can be registered with a default value, their type and authorized values from any package.

Getting a value:

config.GetString("app.name") // "goyave"
config.GetBool("app.debug") // true
config.GetInt("server.port") // 80
config.Has("app.name") // true

Setting a value:

config.Set("app.name", "my awesome app")

Using environment variables:

{
  "database": {
    "host": "${DB_HOST}"
  }
}

Learn more about configuration in the documentation.

Routing

Routing is an essential part of any Goyave application. Routes definition is the action of associating a URI, sometimes having parameters, with a handler which will process the request and respond to it. Separating and naming routes clearly is important to make your API or website clear and expressive.

Routes are defined in routes registrer functions. The main route registrer is passed to goyave.Start() and is executed automatically with a newly created root-level router.

func Register(router *goyave.Router) {
    // Register your routes here

    // With closure, not recommended
    router.Get("/hello", func(response *goyave.Response, r *goyave.Request) {
        response.String(http.StatusOK, "Hi!")
    })

    router.Get("/hello", myHandlerFunction)
    router.Post("/user", user.Register).Validate(user.RegisterRequest)
    router.Route("PUT|PATCH", "/user", user.Update).Validate(user.UpdateRequest)
    router.Route("POST", "/product", product.Store).Validate(product.StoreRequest).Middleware(middleware.Trim)
}

URIs can have parameters, defined using the format {name} or {name:pattern}. If a regular expression pattern is not defined, the matched variable will be anything until the next slash.

Example:

router.Get("/product/{key}", product.Show)
router.Get("/product/{id:[0-9]+}", product.ShowById)
router.Get("/category/{category}/{id:[0-9]+}", category.Show)

Route parameters can be retrieved as a map[string]string in handlers using the request's Params attribute.

func myHandlerFunction(response *goyave.Response, request *goyave.Request) {
    category := request.Params["category"]
    id, _ := strconv.Atoi(request.Params["id"])
    //...
}

Learn more about routing in the documentation.

Controller

Controllers are files containing a collection of Handlers related to a specific feature. Each feature should have its own package. For example, if you have a controller handling user registration, user profiles, etc, you should create a http/controller/user package. Creating a package for each feature has the advantage of cleaning up route definitions a lot and helps keeping a clean structure for your project.

A Handler is a func(*goyave.Response, *goyave.Request). The first parameter lets you write a response, and the second contains all the information extracted from the raw incoming request.

Handlers receive a goyave.Response and a goyave.Request as parameters.
goyave.Request can give you a lot of information about the incoming request, such as its headers, cookies, or body. Learn more here.
goyave.Response implements http.ResponseWriter and is used to write a response. If you didn't write anything before the request lifecycle ends, 204 No Content is automatically written. Learn everything about reponses here.

Let's take a very simple CRUD as an example for a controller definition: http/controller/product/product.go:

func Index(response *goyave.Response, request *goyave.Request) {
    products := []model.Product{}
    result := database.Conn().Find(&products)
    if response.HandleDatabaseError(result) {
        response.JSON(http.StatusOK, products)
    }
}

func Show(response *goyave.Response, request *goyave.Request) {
    product := model.Product{}
    result := database.Conn().First(&product, request.Params["id"])
    if response.HandleDatabaseError(result) {
        response.JSON(http.StatusOK, product)
    }
}

func Store(response *goyave.Response, request *goyave.Request) {
    product := model.Product{
        Name:  request.String("name"),
        Price: request.Numeric("price"),
    }
    if err := database.Conn().Create(&product).Error; err != nil {
        response.Error(err)
    } else {
        response.JSON(http.StatusCreated, map[string]uint{"id": product.ID})
    }
}

func Update(response *goyave.Response, request *goyave.Request) {
    product := model.Product{}
    db := database.Conn()
    result := db.Select("id").First(&product, request.Params["id"])
    if response.HandleDatabaseError(result) {
        if err := db.Model(&product).Update("name", request.String("name")).Error; err != nil {
            response.Error(err)
        }
    }
}

func Destroy(response *goyave.Response, request *goyave.Request) {
    product := model.Product{}
    db := database.Conn()
    result := db.Select("id").First(&product, request.Params["id"])
    if response.HandleDatabaseError(result) {
        if err := db.Delete(&product).Error; err != nil {
            response.Error(err)
        }
    }
}

Learn more about controllers in the documentation.

Middleware

Middleware are handlers executed before the controller handler. They are a convenient way to filter, intercept or alter HTTP requests entering your application. For example, middleware can be used to authenticate users. If the user is not authenticated, a message is sent to the user even before the controller handler is reached. However, if the user is authenticated, the middleware will pass to the next handler. Middleware can also be used to sanitize user inputs, by trimming strings for example, to log all requests into a log file, to automatically add headers to all your responses, etc.

func MyCustomMiddleware(next goyave.Handler) goyave.Handler {
    return func(response *goyave.Response, request *goyave.Request) {
        // Do something
        next(response, request) // Pass to the next handler
    }
}

To assign a middleware to a router, use the router.Middleware() function. Many middleware can be assigned at once. The assignment order is important as middleware will be executed in order.

router.Middleware(middleware.MyCustomMiddleware)

Learn more about middleware in the documentation.

Validation

Goyave provides a powerful, yet easy way to validate all incoming data, no matter its type or its format, thanks to a large number of validation rules.

Incoming requests are validated using rules set, which associate rules with each expected field in the request.

Validation rules can alter the raw data. That means that when you validate a field to be number, if the validation passes, you are ensured that the data you'll be using in your controller handler is a float64. Or if you're validating an IP, you get a net.IP object.

Validation is automatic. You just have to define a rules set and assign it to a route. When the validation doesn't pass, the request is stopped and the validation errors messages are sent as a response.

Rule sets are defined in the same package as the controller, typically in a separate file named request.go. Rule sets are named after the name of the controller handler they will be used with, and end with Request. For example, a rule set for the Store handler will be named StoreRequest. If a rule set can be used for multiple handlers, consider using a name suited for all of them. The rules for a store operation are often the same for update operations, so instead of duplicating the set, create one unique set called UpsertRequest.

Example: (http/controller/product/request.go)

var (
    StoreRequest = validation.RuleSet{
        "name":  validation.List{"required", "string", "between:3,50"},
        "price": validation.List{"required", "numeric", "min:0.01"},
        "image": validation.List{"nullable", "file", "image", "max:2048", "count:1"},
    }

    // ...
)

Once your rules sets are defined, you need to assign them to your routes using the Validate() method.

router.Post("/product", product.Store).Validate(product.StoreRequest)

Learn more about validation in the documentation.

Database

Most web applications use a database. In this section, we are going to see how Goyave applications can query a database, using the awesome Gorm ORM.

Database connections are managed by the framework and are long-lived. When the server shuts down, the database connections are closed automatically. So you don't have to worry about creating, closing or refreshing database connections in your application.

Very few code is required to get started with databases. There are some configuration options that you need to change though:

  • database.connection
  • database.host
  • database.port
  • database.name
  • database.username
  • database.password
  • database.options
  • database.maxOpenConnection
  • database.maxIdleConnection
  • database.maxLifetime
user := model.User{}
db := database.Conn()
db.First(&user)

fmt.Println(user)

Models are usually just normal Golang structs, basic Go types, or pointers of them. sql.Scanner and driver.Valuer interfaces are also supported.

func init() {
    database.RegisterModel(&User{})
}

type User struct {
    gorm.Model
    Name         string
    Age          sql.NullInt64
    Birthday     *time.Time
    Email        string  `gorm:"type:varchar(100);uniqueIndex"`
    Role         string  `gorm:"size:255"` // set field size to 255
    MemberNumber *string `gorm:"unique;not null"` // set member number to unique and not null
    Num          int     `gorm:"autoIncrement"` // set num to auto incrementable
    Address      string  `gorm:"index:addr"` // create index with name `addr` for address
    IgnoreMe     int     `gorm:"-"` // ignore this field
}

Learn more about using databases in the documentation.

Localization

The Goyave framework provides a convenient way to support multiple languages within your application. Out of the box, Goyave only provides the en-US language.

Language files are stored in the resources/lang directory.

.
└── resources
 Β Β  └── lang
 Β Β   Β Β  └── en-US (language name)
 Β Β   Β Β      β”œβ”€β”€ fields.json (optional)
 Β Β   Β Β      β”œβ”€β”€ locale.json (optional)
 Β Β   Β Β      └── rules.json (optional)

The fields.json file contains the field names translations and their rule-specific messages. Translating field names helps making more expressive messages instead of showing the technical field name to the user. Rule-specific messages let you override a validation rule message for a specific field.

Example:

{
    "email": {
        "name": "email address",
        "rules": {
            "required": "You must provide an :field."
        }
    }
}

The locale.json file contains all language lines that are not related to validation. This is the place where you should write the language lines for your user interface or for the messages returned by your controllers.

Example:

{
    "product.created": "The product have been created with success.",
    "product.deleted": "The product have been deleted with success."
}

The rules.json file contains the validation rules messages. These messages can have placeholders, which will be automatically replaced by the validator with dynamic values. If you write custom validation rules, their messages shall be written in this file.

Example:

{
    "integer": "The :field must be an integer.",
    "starts_with": "The :field must start with one of the following values: :values.",
    "same": "The :field and the :other must match."
}

When an incoming request enters your application, the core language middleware checks if the Accept-Language header is set, and set the goyave.Request's Lang attribute accordingly. Localization is handled automatically by the validator.

func ControllerHandler(response *goyave.Response, request *goyave.Request) {
    response.String(http.StatusOK, lang.Get(request.Lang, "my-custom-message"))
}

Learn more about localization in the documentation.

Testing

Goyave provides an API to ease the unit and functional testing of your application. This API is an extension of testify. goyave.TestSuite inherits from testify's suite.Suite, and sets up the environment for you. That means:

  • GOYAVE_ENV environment variable is set to test and restored to its original value when the suite is done.
  • All tests are run using your project's root as working directory. This directory is determined by the presence of a go.mod file.
  • Config and language files are loaded before the tests start. As the environment is set to test, you need a config.test.json in the root directory of your project.

This setup is done by the function goyave.RunTest, so you shouldn't run your test suites using testify's suite.Run() function.

The following example is a functional test and would be located in the test package.

import (
    "github.com/username/projectname/http/route"
    "goyave.dev/goyave/v4"
)

type CustomTestSuite struct {
    goyave.TestSuite
}

func (suite *CustomTestSuite) TestHello() {
    suite.RunServer(route.Register, func() {
        resp, err := suite.Get("/hello", nil)
        suite.Nil(err)
        suite.NotNil(resp)
        if resp != nil {
            defer resp.Body.Close()
            suite.Equal(200, resp.StatusCode)
            suite.Equal("Hi!", string(suite.GetBody(resp)))
        }
    })
}

func TestCustomSuite(t *testing.T) {
    goyave.RunTest(t, new(CustomTestSuite))
}

When writing functional tests, you can retrieve the response body easily using suite.GetBody(response).

resp, err := suite.Get("/get", nil)
suite.Nil(err)
if err == nil {
    defer resp.Body.Close()
    suite.Equal("response content", string(suite.GetBody(resp)))
}

URL-encoded requests:

headers := map[string]string{"Content-Type": "application/x-www-form-urlencoded; param=value"}
resp, err := suite.Post("/product", headers, strings.NewReader("field=value"))
suite.Nil(err)
if err == nil {
    defer resp.Body.Close()
    suite.Equal("response content", string(suite.GetBody(resp)))
}

JSON requests:

headers := map[string]string{"Content-Type": "application/json"}
body, _ := json.Marshal(map[string]interface{}{"name": "Pizza", "price": 12.5})
resp, err := suite.Post("/product", headers, bytes.NewReader(body))
suite.Nil(err)
if err == nil {
    defer resp.Body.Close()
    suite.Equal("response content", string(suite.GetBody(resp)))
}

Testing JSON response:

suite.RunServer(route.Register, func() {
    resp, err := suite.Get("/product", nil)
    suite.Nil(err)
    if err == nil {
        defer resp.Body.Close()
        json := map[string]interface{}{}
        err := suite.GetJSONBody(resp, &json)
        suite.Nil(err)
        if err == nil { // You should always check parsing error before continuing.
            suite.Equal("value", json["field"])
            suite.Equal(float64(42), json["number"])
        }
    }
})

The testing API has many more features such as record generators, factories, database helpers, a middleware tester, support for multipart and file uploads...

Learn more about testing in the documentation.

Status handlers

Status handlers are regular handlers executed during the finalization step of the request's lifecycle if the response body is empty but a status code has been set. Status handler are mainly used to implement a custom behavior for user or server errors (400 and 500 status codes).

The following file http/controller/status/status.go is an example of custom 404 error handling:

package status

import "goyave.dev/goyave/v4"

func NotFound(response *goyave.Response, request *goyave.Request) {
    if err := response.RenderHTML(response.GetStatus(), "errors/404.html", nil); err != nil {
        response.Error(err)
    }
}

Status handlers are registered in the router.

// Use "status.NotFound" for empty responses having status 404 or 405.
router.StatusHandler(status.NotFound, 404)

Learn more about status handlers in the documentation.

CORS

Goyave provides a built-in CORS module. CORS options are set on routers. If the passed options are not nil, the CORS core middleware is automatically added.

router.CORS(cors.Default())

CORS options should be defined before middleware and route definition. All of this router's sub-routers inherit CORS options by default. If you want to remove the options from a sub-router, or use different ones, simply create another cors.Options object and assign it.

cors.Default() can be used as a starting point for custom configuration.

options := cors.Default()
options.AllowedOrigins = []string{"https://google.com", "https://images.google.com"}
router.CORS(options)

Learn more about CORS in the documentation.

Authentication

Goyave provides a convenient and expandable way of handling authentication in your application. Authentication can be enabled when registering your routes:

import "goyave.dev/goyave/v4/auth"

//...

authenticator := auth.Middleware(&model.User{}, &auth.BasicAuthenticator{})
router.Middleware(authenticator)

Authentication is handled by a simple middleware calling an Authenticator. This middleware also needs a model, which will be used to fetch user information on a successful login.

Authenticators use their model's struct fields tags to know which field to use for username and password. To make your model compatible with authentication, you must add the auth:"username" and auth:"password" tags:

type User struct {
    gorm.Model
    Email    string `gorm:"type:char(100);uniqueIndex" auth:"username"`
    Name     string `gorm:"type:char(100)"`
    Password string `gorm:"type:char(60)" auth:"password"`
}

When a user is successfully authenticated on a protected route, its information is available in the controller handler, through the request User field.

func Hello(response *goyave.Response, request *goyave.Request) {
    user := request.User.(*model.User)
    response.String(http.StatusOK, "Hello " + user.Name)
}

Learn more about authentication in the documentation.

Rate limiting

Rate limiting is a crucial part of public API development. If you want to protect your data from being crawled, protect yourself from DDOS attacks, or provide different tiers of access to your API, you can do it using Goyave's built-in rate limiting middleware.

This middleware uses either a client's IP or an authenticated client's ID (or any other way of identifying a client you may need) and maps a quota, a quota duration and a request count to it. If a client exceeds the request quota in the given quota duration, this middleware will block and return HTTP 429 Too Many Requests.

The middleware will always add the following headers to the response:

  • RateLimit-Limit: containing the requests quota in the time window
  • RateLimit-Remaining: containing the remaining requests quota in the current window
  • RateLimit-Reset: containing the time remaining in the current window, specified in seconds
import "goyave.dev/goyave/v4/middleware/ratelimiter"

ratelimiterMiddleware := ratelimiter.New(func(request *goyave.Request) ratelimiter.Config {
    return ratelimiter.Config {
        RequestQuota:  100,
        QuotaDuration: time.Minute,
        // 100 requests per minute allowed
        // Client IP will be used as identifier
    }
})

router.Middleware(ratelimiterMiddleware)

Learn more about rate limiting in the documentation.

Websocket

Goyave is using gorilla/websocket and adds a layer of abstraction to it to make it easier to use. You don't have to write the connection upgrading logic nor the close handshake. Just like regular HTTP handlers, websocket handlers benefit from reliable error handling and panic recovery.

Here is an example of an "echo" feature:

upgrader := websocket.Upgrader{}
router.Get("/websocket", upgrader.Handler(func(c *websocket.Conn, request *goyave.Request) error {
    for {
        mt, message, err := c.ReadMessage()
        if err != nil {
            return err
        }
        goyave.Logger.Printf("recv: %s", message)
        err = c.WriteMessage(mt, message)
        if err != nil {
            return fmt.Errorf("write: %w", err)
        }
    }
}))

Learn more about websockets in the documentation.

Contributing

Thank you for considering contributing to the Goyave framework! You can find the contribution guide in the documentation.

I have many ideas for the future of Goyave. I would be infinitely grateful to whoever want to support me and let me continue working on Goyave and making it better and better.

You can support me on Github Sponsor.

❀ Sponsor me!

I'm very grateful to my patrons, sponsors and donators:

  • Ben Hyrman
  • Massimiliano Bertinetti
  • ethicnology
  • Yariya
  • sebastien-d-me
  • NebojΕ‘a KoturoviΔ‡

Contributors

A big "Thank you" to the Goyave contributors:

Used by

Adagio.io

Do you want to be featured here? Open an issue.

License

The Goyave framework is MIT Licensed. Copyright Β© 2019 JΓ©rΓ©my LAMBERT (SystemGlitch)

goyave's People

Contributors

agbaraka avatar alexandregv avatar ethicnology avatar gmgalvan avatar imuni4fun avatar jrimbault avatar morishiri avatar system-glitch avatar zaosoula avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

goyave's Issues

Mispelled "Learning" in README.

Information

On the Table of Contents, the first link says Leaning, instead of Learning. It is missing the letter 'r' after the letter 'a'.

Can't read json raw request

Description

Version: v2.2.0
Environment: Mac Catalina, go1.13.4
Reproduces: always

Hi,

I tried to handle raw json request with Request.Data["key"], but got nil, first I sent this as my request on postman body raw json:

{
    "name": "John Doe",
    "tags": ["tag1", "tag2"]
}

Then I log print out the result, got this:

map[]

And I also tried the Request.Integer(), Request.Bool() and some of them got error like this:

#Case Request.Integer()
Field \"test\" is not an integer

#Case Request.Bool()
Field \"test\" is not a bool

And this is log on terminal:

2020/01/02 12:16:49 Server is running on port: 8090
2020/01/02 12:16:54 Field "test" is not an integer
2020/01/02 12:16:54 Field "test" is not an integer
goroutine 35 [running]: runtime/debug.Stack(0x10e47ec, 0xc0000b2000, 0x2)
    /usr/local/Cellar/go/1.13.4/libexec/src/runtime/debug/stack.go:24 +0x9d
runtime/debug.PrintStack()
    /usr/local/Cellar/go/1.13.4/libexec/src/runtime/debug/stack.go:16 +0x22
github.com/System-Glitch/goyave/v2.(*Response).Error(0xc00000e0a0, 0x148e4a0, 0xc000020190, 0xc0000c3748, 0x100dbc6)
    /Users/ridwankustanto/go/pkg/mod/github.com/!system-!glitch/goyave/[email protected]/response.go:118 +0x96
github.com/System-Glitch/goyave/v2.recoveryMiddleware.func1.1(0xc00000e0a0)
    
 /Users/ridwankustanto/go/pkg/mod/github.com/!system-!glitch/goyave/[email protected]/middleware.go:25 +0x4b
panic(0x148e4a0, 0xc000020190)
    /usr/local/Cellar/go/1.13.4/libexec/src/runtime/panic.go:679 +0x1b2
log.Panicf(0x1549f5c, 0x1c, 0xc0000c3810, 0x1, 0x1)
    /usr/local/Cellar/go/1.13.4/libexec/src/log/log.go:345 +0xc0
github.com/System-Glitch/goyave/v2.(*Request).Integer(...)
    /Users/ridwankustanto/go/pkg/mod/github.com/!system-!glitch/goyave/[email protected]/request.go:127
karmapala-backend/http/controller/user.Register(0xc00000e0a0, 0xc0002960a0)
    /Users/ridwankustanto/go/src/gitlab.com/ridwankustanto/karmapala-backend/http/controller/user/user.go:24 +0x137
github.com/System-Glitch/goyave/v2.validateRequestMiddleware.func1(0xc00000e0a0, 0xc0002960a0)
    

/Users/ridwankustanto/go/pkg/mod/github.com/!system-!glitch/goyave/[email protected]/middleware.go:106 +0xa5
karmapala-backend/http/middleware.EnableCORS.func1(0xc00000e0a0, 0xc0002960a0)
    /Users/ridwankustanto/go/src/gitlab.com/ridwankustanto/karmapala-backend/http/middleware/cors.go:26 +0x580
github.com/System-Glitch/goyave/v2.languageMiddleware.func1(0xc00000e0a0, 0xc0002960a0)
    /Users/ridwankustanto/go/pkg/mod/github.com/!system-!glitch/goyave/[email protected]/middleware.go:139 +0xa9
 github.com/System-Glitch/goyave/v2.parseRequestMiddleware.func1(0xc00000e0a0, 0xc0002960a0)
    /Users/ridwankustanto/go/pkg/mod/github.com/!system-!glitch/goyave/[email protected]/middleware.go:56 +0x118
github.com/System-Glitch/goyave/v2.recoveryMiddleware.func1(0xc00000e0a0, 0xc0002960a0)
    /Users/ridwankustanto/go/pkg/mod/github.com/!system-!glitch/goyave/[email protected]/middleware.go:29 +0x77
github.com/System-Glitch/goyave/v2.(*Router).requestHandler(0xc0002545a0, 0x15f2220, 0xc0002c6000, 0xc0002a6200, 0x1561998, 0x0)
    /Users/ridwankustanto/go/pkg/mod/github.com/!system-!glitch/goyave/[email protected]/router.go:120 +0x147
github.com/System-Glitch/goyave/v2.(*Router).Route.func1(0x15f2220, 0xc0002c6000, 0xc0002a6200)
    /Users/ridwankustanto/go/pkg/mod/github.com/!system-!glitch/goyave/[email protected]/router.go:55 +0x5a
net/http.HandlerFunc.ServeHTTP(0xc0002547e0, 0x15f2220, 0xc0002c6000, 0xc0002a6200)
    /usr/local/Cellar/go/1.13.4/libexec/src/net/http/server.go:2007 +0x44
github.com/gorilla/mux.(*Router).ServeHTTP(0xc0000d8600, 0x15f2220, 0xc0002c6000, 0xc0002c0000)
    /Users/ridwankustanto/go/pkg/mod/github.com/gorilla/[email protected]/mux.go:212 +0xe2
net/http.serverHandler.ServeHTTP(0xc000248380, 0x15f2220, 0xc0002c6000, 0xc0002c0000)
    /usr/local/Cellar/go/1.13.4/libexec/src/net/http/server.go:2802 +0xa4
net/http.(*conn).serve(0xc0002b0000, 0x15f36e0, 0xc0002bc000)
    /usr/local/Cellar/go/1.13.4/libexec/src/net/http/server.go:1890 +0x875
created by net/http.(*Server).Serve
    /usr/local/Cellar/go/1.13.4/libexec/src/net/http/server.go:2927 +0x38e

Expected result:
Got value on a specific key.

Anyway, happy new year everyone!! πŸŽ‰πŸŽ‰πŸŽ‰

Custom HTTP request error handler such as 404 handler etc

Description

In my case, I want to customize my Http request error handler, for example the default 404 response is 404 page not found. I need to customize it to a JSON response like

{
 "error": true,
 "message": "Page not found",
 "code": 404
}

Authentication

Is there already an authentication system backed in? I couldn't find anything.

OpenAPI generator

Proposal

Develop a module able to generate OpenAPI specification for Goyave applications by reading the main route registrer and validation rule sets.

This feature could be included in the CLI Utility ( #39 )

Possible drawbacks

None.

Additional information

This issue is a feature proposal and is meant to be discussed.
It is also a good candidate if you want to contribute to the project.
This should probably be developed as a side-project.

"exists" validation rule

Proposal

The "unique" validation rules checks if a resource doesn't exist. If you just want to know if a resource exists, you currently have to implement a custom validation rule. I'd like to add the "exists" validation rule into the framework directly.

Possible drawbacks

None.

Error "pq: SSL is not enabled on the server" when using Postgres

Description

Version: v2.3.0
Environment: [OS Windows 10, Go version go1.12.5]

Now I'm using Postgres as my database client, in config.json I already add dbOptions sslmode=disable, but the error always occurred.

Relevant logs: (if needed)

2020/01/02 17:05:36 pq: SSL is not enabled on the server
goroutine 21 [running]:
runtime/debug.Stack(0x4ebc43, 0xc00008c000, 0x2)
        C:/Go/src/runtime/debug/stack.go:24 +0xa4
runtime/debug.PrintStack()
        C:/Go/src/runtime/debug/stack.go:16 +0x29
github.com/System-Glitch/goyave/v2.(*Response).Error(0xc0001d6920, 0x82b6c0, 0xc0001864e0, 0xc000079638, 0x0)
        C:/Users/Frontend/go/pkg/mod/github.com/!system-!glitch/goyave/[email protected]/response.go:118 +0x9d
github.com/System-Glitch/goyave/v2.recoveryMiddleware.func1.1(0xc0001d6920)
        C:/Users/Frontend/go/pkg/mod/github.com/!system-!glitch/goyave/[email protected]/middleware.go:25 +0x52
panic(0x82b6c0, 0xc0001864e0)
        C:/Go/src/runtime/panic.go:522 +0x1c3
github.com/System-Glitch/goyave/v2/database.newConnection(0x894e00)
        C:/Users/Frontend/go/pkg/mod/github.com/!system-!glitch/goyave/[email protected]/database/database.go:73 +0x2c7
github.com/System-Glitch/goyave/v2/database.GetConnection(...)
        C:/Users/Frontend/go/pkg/mod/github.com/!system-!glitch/goyave/[email protected]/database/database.go:22
spp-online/database/model.User.GetBy(0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, ...)
        D:/Ervan/Ext/spp-online/database/model/user.go:56 +0x284
spp-online/http/controller/auth.signIn(0xc0001d6920, 0xc0001727d0)
        D:/Ervan/Ext/spp-online/http/controller/auth/auth.go:23 +0x91
github.com/System-Glitch/goyave/v2.validateRequestMiddleware.func1(0xc0001d6920, 0xc0001727d0)
        C:/Users/Frontend/go/pkg/mod/github.com/!system-!glitch/goyave/[email protected]/middleware.go:135 +0xac
github.com/System-Glitch/goyave/v2.corsMiddleware.func1(0xc0001d6920, 0xc0001727d0)
        C:/Users/Frontend/go/pkg/mod/github.com/!system-!glitch/goyave/[email protected]/middleware.go:84 +0x160
github.com/System-Glitch/goyave/v2.languageMiddleware.func1(0xc0001d6920, 0xc0001727d0)
        C:/Users/Frontend/go/pkg/mod/github.com/!system-!glitch/goyave/[email protected]/middleware.go:168 +0xb2
github.com/System-Glitch/goyave/v2.parseRequestMiddleware.func1(0xc0001d6920, 0xc0001727d0)
        C:/Users/Frontend/go/pkg/mod/github.com/!system-!glitch/goyave/[email protected]/middleware.go:56 +0xfa
github.com/System-Glitch/goyave/v2.recoveryMiddleware.func1(0xc0001d6920, 0xc0001727d0)
        C:/Users/Frontend/go/pkg/mod/github.com/!system-!glitch/goyave/[email protected]/middleware.go:29 +0x70
github.com/System-Glitch/goyave/v2.(*Router).requestHandler(0xc000171170, 0x940b00, 0xc0001e40e0, 0xc000180b00, 0x8c21b8, 0xc0001709c0)
        C:/Users/Frontend/go/pkg/mod/github.com/!system-!glitch/goyave/[email protected]/router.go:149 +0x165
github.com/System-Glitch/goyave/v2.(*Router).route.func1(0x940b00, 0xc0001e40e0, 0xc000180b00)
        C:/Users/Frontend/go/pkg/mod/github.com/!system-!glitch/goyave/[email protected]/router.go:73 +0x61
net/http.HandlerFunc.ServeHTTP(0xc0001d6680, 0x940b00, 0xc0001e40e0, 0xc000180b00)
        C:/Go/src/net/http/server.go:1995 +0x4b
github.com/gorilla/mux.(*Router).ServeHTTP(0xc0000e8540, 0x940b00, 0xc0001e40e0, 0xc000206000)
        C:/Users/Frontend/go/pkg/mod/github.com/gorilla/[email protected]/mux.go:212 +0xea
net/http.serverHandler.ServeHTTP(0xc000050ea0, 0x940b00, 0xc0001e40e0, 0xc000206000)
        C:/Go/src/net/http/server.go:2774 +0xaf
net/http.(*conn).serve(0xc00016d540, 0x941900, 0xc0001ea040)
        C:/Go/src/net/http/server.go:1878 +0x858
created by net/http.(*Server).Serve
        C:/Go/src/net/http/server.go:2884 +0x2fb

How it occurred

Every querying data

How to reproduce

Every querying data

JSON Schema Validator

Other than your input validator, consider the JSON Schema validator (jsonschema.org). Although similar, it has the advantage of write-once, run anywhere. The same validation schema could run on the browser before bothering the server. There would be no challenge keeping the two in synchronization: just use the same validator file.

Failed to Handle CORS

Description

Version: v2.2.0
Commit: bc72e19
Version: v2.2.0
Environment: Mac Catalina, go1.13.4

Hi, I just noticed this when I tried to consume API within my vuejs app, it always blocked by CORS. I did use package "github.com/rs/cors" on middleware, but still can't solve it. And it said "OPTIONS 405 (Method Not Allowed" which I guess I handled on middleware.

first, I tried manual way to handle cors through middleware:

func EnableCORS(next goyave.Handler) goyave.Handler {

return func(response *goyave.Response, request *goyave.Request) {

	(*response).Header().Set("Access-Control-Allow-Origin", "*")
	(*response).Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
	(*response).Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
	(*response).Header().Set("Content-Type", "application/json")
	(*response).Header().Set("Content-Type", "text/html; charset=utf-8")
	(*response).Header().Set("Access-Control-Allow-Credentials", "true")

	log.Println(request.Method())
	if (*request).Method() == "OPTIONS" {
		response.WriteHeader(http.StatusOK)
		return
	}

	next(response, request) // Pass to the next handler
}

}

And I tried to use this pkg too "github.com/rs/cors" on the route register:

routers := router.Subrouter("/")
middlewareCors := goyave.NativeMiddleware(func(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handler := cors.AllowAll().Handler(next)

		if r.Method == "OPTIONS" {
			w.WriteHeader(http.StatusOK)
			return
		}
		handler.ServeHTTP(w, r) // Don't call "next" if your middleware is blocking.
	})
})
routers.Middleware(middlewareCors)

But none of them can solve cors or handle Preflight request. Or I miss something...

Can you give me example of handling cors with goyave? That would be great.

Thanks!

Template rendering

Hi,
I am wondering if there is a method like response.JSON() for template rendering from package text/template
for example like
response.Render(http.StatusOK, "template", &dataStructure)

Thank you.

Embedded static resources

Proposal

Go's 1.16 new embed feature is great to bundle static resources into the binary, making it easier to distribute it. Currently, the frameworks relies on a non-interfaced filesystem, which makes it a bit harder to use embedded resources. Only the configuration supports it. I would like to provide a solution for developers wanting to embed their files.

  • Create a new handler similar to router.Static(), but taking io/fs.FS as a parameter.
  • Add the ability to use an io/fs.FS to load language files. (This may require a bit of refactoring into the framework so it's possible to load a language file by file)
  • Provide an abstracted version of the helper/filesystem package so it works with io/fs too.
  • Allow Request to use generic file system.

On top of adding the ability to embed files, this would also let developers use virtual filesystems and fetch files from other sources more easily. However, the io/fs packge is still a bit young in my opinion, and lacks interfaces for writable FS. I wouldn't want to create a temporary non-standard interface that will probably not be compatible with a potential future standard one. So for now, this issue will stay on standby until more development on the io/fs package.

Because the embed feature is only available in go 1.16+ and that the io.FS interface is required for what I would like to achieve here, we can't build this into the framework without breaking compatibility with prior versions. I don't want to stop supporting older versions (yet), so we could create an external library. Later on, we could consider merging this library into the framework. With v4, I dropped go versions < 1.16.

Possible drawbacks

None.

Additional information

This issue is a feature proposal and is meant to be discussed. The design is not set yet neither.
It is also a good candidate if you want to contribute to the project.

Rate limiting

Proposal

To make rate limiting easier, a middleware could be developed. This middleware would use a new config entry making it possible to adjust the rate easily. The real goal is to make a basic rate limiter middleware, which would cover most cases and be easy to use, mostly to protect from attacks.

The middleware will have to correctly set the following headers, following this IETF spec:

  • RateLimit-Limit: the rate limit ceiling
  • RateLimit-Remaining: the number of requests left
  • RateLimit-Reset: the number of seconds until the quota resets

Note: the IETF spec doesn't use the X- header prefix, unlike popular APIs such as Github's. It would be worth making some research to know if it would be better to add the X- prefix or keep it as defined in the IETF spec.

In case of cancelled request because of limiting, the middleware would be stopping (don't call next()) and would respond with the HTTP status 429 Too Many Requests, in accordance with RFC 6585. The default status handler can be kept and developers will be able to define the behavior of their choice when a request is rate limited.

Limiting will be done using an in-memory storage directly implemented in the middleware. A simple map and a struct containing the user's information (IP, time frame, remaining requests) are probably more than enough. The storage needs to be thread-safe, as requests can be served in parallel.

The official go package golang.org/x/time/rate could be used, but it would need to be extended a little bit to support multiple clients. We want to limit IPs that keep requesting the server, but not the other legitimate users.

I would like to start with a simple rate limiter, as mentioned above, but a more advanced rate limiter could be developed next to it later on. There is no design or clear requirements specification for it yet. For example, this advanced rate limiter could:

  • work with authenticated users and add variable limits depending on a user plan
  • be configured to count rates in relation to a route
  • use an external storage driver instead of the in-memory one explained earlier (support the database for example)
  • check the X-Forwarded-For and X-Real-IP headers if the application is running behind a reverse proxy
  • and more?

Either way, this middleware has to be compliant with previously mentioned IETF spec.

Possible drawbacks

Rate limiting can have a slight impact of performance, and especially on memory usage if using in-memory storage. However, it is a mandatory feature for public APIs and well worth the upfront cost.

Additional information

This issue is a feature proposal and is meant to be discussed.
It is also a good candidate if you want to contribute to the project.

Permission system (guards)

Proposal

Guards are an extension of the authentication system using user-defined fields from the authenticator's user model to allow or deny specific actions to an authenticated user.

For example, a Goyave application will define a guard for forums moderation. A moderator is allowed to modify the posts of other users, but regular users aren't. A guard update-others will be implemented, using the IsModerator field from the user model and the route parameter to check if the user is a moderator or if its own post.

Guards could be used in two ways:

  • As function calls, so they can be used anywhere such as in the middle of a controller handler
  • In middleware to protect routes directly without cluttering controller handlers.

This system could support OAuth in some way too.

The implementation is not defined yet, so feel free to discuss and suggest one.

Possible drawbacks

None.

Additional information

This issue is a feature proposal and is meant to be discussed.
It is also a good candidate if you want to contribute to the project.

Failed to Dockerize

Description

Version: v2.2.0
Commit: bc72e19

Description

Hi, thank you for making this framework! I found it on Medium post, and it looks promising and I would give it a try for my next project.

But recently, I try to install and gave log a bit for knowning what app and port that I was running on with goyave config, everything was fine, till I try to dockerize this app. It was running but the config file fell into default.json, I use FROM alpine:3.9 as my production build phase:

FROM golang:alpine as builder
.
.
.

FROM alpine:3.9

RUN apk add --no-cache tzdata
ENV TZ Asia/Jakarta

COPY --from=builder /go/pkg/mod/github.com/!system-!glitch/goyave/ /go/pkg/mod/github.com/!system-!glitch/goyave/
COPY --from=builder /app/config.json /app/
COPY --from=builder /app/resources /app/resources
COPY --from=builder /app/app /app/app

# Run the binary.
ENTRYPOINT ["/app/app"] 

docker-compose up and the container was running, but when I hit the default port for localhost:8080, the server not actually running.

Did I do wrong? What files that I need to copy in order to make the binary runs? as you can see I already copy config.json and resources as you mentioned on documents.

Thanks once again! 😁

v3 Discussion

v2.10.1 is expected to be the last release before v3, unless critical issues have to be addressed while developing this new major version.

v3 will contain a number of breaking changes. To avoid moving to a new major version too often, required breaking changes are pushed back as much as possible and grouped into a single release.

For the moment, v3 will contain the following changes:

  • Revamped configuration.
    • Maybe using another configuration format such as toml instead of json
    • Improving the way configuration is organized, by letting the possibility of grouping items together (for example grouping all database config into a single category) instead of accumulating a lot of entries at the root-level.
    • Ease plugin development by adding a possibility to add entries to the default config in an init() function
    • Make configuration checked at compile-time by using structures instead of maps.
  • Validation
    • Improve performance by parsing rule sets at startup time instead of during each request.
    • Add the ability to define validation in a more complex way, which will be more verbose, but more flexible and checked at compile-time. This would solve the comma escaping problem in rules parameters.
  • Route parameter converters ( #93 ) may require a breaking change.
  • Remove native handlers. I understand that it would make it harder to plug well known go http modules to Goyave and isolate it a bit from the rest of the environment, but this change would be beneficial in many ways:
    • Performance improvements: no need to duplicate the request body
    • Consistency: plugged modules may not work well with Goyave and there is no way to guarantee it. They probably don't follow the same design choices and principles, which can make programs inconsistent and sometimes unreliable.
  • Convention changes
    • Remove the request package and move rule sets to a requests.go file inside the controller packages. This change would make route definition cleaner and easier, and the directory structure lighter. The requests package naming was inconvenient too.
  • More flexibility for databases. For example, the ability to add another dialect will be added.

Before the development of v3 starts, please let me know if you think of any breaking or major change that you think would be good for the framework, and feel free to discuss the points above.

Inconsistent validation results

Bug report

Version: v3.8.0
Environment: Ubuntu 20.04, Go 1.16
Reproduces: rarely

Because validation.Rules is a map, iteration order is not guaranteed, meaning that rules that rely on a certain order of validation (such as dates before/after) and where conversion is needed will randomly not pass even if the request is valid.

Potential fix: pre-process sorted list of keys by checking presence of field comparison rules

Gorm settings

Support

Version: v2.7.0

Hello, is there a way override basic Gorm settings in the global database connection scope, for example auto_preload feature?

Validating arrays of objects

Proposal

It is currently not possible to validate the fields of each object in an array of objects. We would like the ability to validate the following body:

{
    "array": [
        {
            "field1": "string",
            "field2": 2,
            "field3": {"subfield": "string"},
            "field4": [1,2,3],
            "field5": [
                {"subfield": "string"}
            ],
        }
    ]
}

Possible drawbacks

Slightly slower startup time.

Validation "Required if"

Proposal

Add a "required_if" validation rule that would take another field as first parameter and check if its value is exactly the value of the second parameter (converted to proper type). If that is true, then the field under validation is required.

"credit_card_number": {"required_if:payment_type,cc"}

Possible drawbacks

None.

Logging

Proposal

Goyave currently doesn't provide any built-in way to log activity or errors. You can however implement a middleware yourself and use chained writers if needed. Logging is an important part of all applications so a flexible implementation should be added to the framework. How logging is made in an application should still be decided by the developer, not the framework.

One or multiple interfaces will be needed and they should fit most (or all) popular logging libraries (such as logrus). A default logger can be provided.

The logging feature should allow:

  • logging of incoming requests
  • logging of responses
  • logging of debug, info, errors on-demand in handlers and middleware
  • custom logging behavior in case of unexpected error
  • set output (stdout, stderr, files, other)

The implementation is not defined yet, so feel free to discuss and suggest one.

Possible drawbacks

None.

Additional information

This issue is a feature proposal and is meant to be discussed.
It is also a good candidate if you want to contribute to the project.

Root route ("/") always return 404

Bug report

Version: v3.0.0
Environment: Mac OS Catalina, Go version go1.15.1 darwin/amd64
Reproduces: always

Setting a route poining to the root route ("/") return

 {"error":"Not Found"}

Relevant logs: (if needed)

No logs present

How it occurred

Setting a route in route.go:

router.Get("/", page.Index)

Expected result:
Describe the expected result and how it differs from the actual result.

How to reproduce

Point any route to "/" and start the server, then open browser and point to http://127.0.0.1:8080/

Failed to Dockerize

Description

Version: v2.7.0
Environment: Mac Os, 1.13
Reproduces: always

Hello,

There already was an issue called Failed to Dockerize which has been solved, but I've run into a different problem with dockerizing the goyave-template. It's important to note that everything works if it's running without Docker.

When I start a server, running inside a container, and I call /hello endpoint I get curl: (52) Empty reply from server response. When I start server just running by go run goyave-template, and I call /hello endpoint I get Hi! response.

I tried put logging inside Start function and I can see that the server is running. What interesting is that if I put logging inside the SayHi I got nothing in the terminal. So I suspect that the handler isn't even called. Also, I tried to use a closure. No success.

I haven't tried my Dockerize method with the prev version v2.2.0 in which the original issue was reported.

All files such as config.json, docker-compose and Dockerfile, are copy&paste in this description.

How it occurred

Ghosts-MacBook-Pro-2:goyave-docker ghost$ curl -I localhost:8080/hello
curl: (52) Empty reply from server

Expected result:
Based on the documentation, it should return Hi! message.

How to reproduce

This is my docker-compose.yml file:

version: '3.2'

services:
  api:
    container_name: goyave_docker_api
    build: .
    ports:
      - 8080:8080

  vgo:
    container_name: goyave_docker_vgo
    image: golang:1.13
    volumes:
      - .:/app
    working_dir: /app

This is my Dockerfile:

FROM golang:1.13-alpine3.11 as builder

WORKDIR /app

COPY . .

RUN CGO_ENABLED=0 GOOS=linux go build -mod=vendor -o api

FROM alpine:3.11

WORKDIR /app

RUN apk add --no-cache tzdata
RUN apk add --no-cache ca-certificates

ENV TZ Europe/Prague

COPY --from=builder /app/api ./api
COPY resources /app/resources
COPY config.json ./config.json

EXPOSE 8080

ENTRYPOINT [ "./api" ]

This is my config.json file:

{
    "appName": "goyave-docker",
    "environment": "localhost",
    "domain": "",
    "maintenance": false,
    "host": "localhost",
    "port": 8080,
    "httpsPort": 8081,
    "protocol": "http",
    "tlsCert": "",
    "tlsKey": "",
    "debug": false,
    "timeout": 10,
    "maxUploadSize": 10,
    "defaultLanguage": "en-US",
    "dbConnection": "none",
    "dbHost": "127.0.0.1",
    "dbPort": 3306,
    "dbName": "goyave",
    "dbUsername": "root",
    "dbPassword": "root",
    "dbOptions": "charset=utf8&parseTime=true&loc=Local",
    "dbMaxOpenConnections": 20,
    "dbMaxIdleConnections": 20,
    "dbMaxLifetime": 300,
    "dbAutoMigrate": false
}

In the config.json, I changed host key from 127.0.0.1 to localhost.

Then I run:

  1. docker-compose run --rm vgo go get && go mod vendor to create the vendor folder
  2. docker-compose up -d --build to build an image a run a server

Validation rules composition

Proposal

Sometimes it could be handy to be able to use another rule set inside a larger rule set by using composition. Example:

CreateUserRequest = validation.RuleSet{
    "email": {"required", "email"},
    "password": {"required", "min:6"},
}

CreateManyUsersRequest = validation.RuleSet{
    "users": {"required", "array:object"},
    "users[]": CreateUserRequest,
}

Note: could use an interface so both types ([]string{} and RuleSet) can be used as a value inside a RuleSet. Don't forget about alternative syntax too!

Possible drawbacks

None.

CLI Utility

Proposal

Create a CLI Utility called goyave-cli or gyv to make Goyave development easier. The utility would be an interactive, a bit like gh

  • Creation tools:
    • Create a project
      • The utility would ask a series of questions
      • Name of the project, name of the go module
      • Goyave version (latest by default)
      • Database used (changes the config options)
    • Create controllers
      • Resource controllers (full CRUD template, then fill the blanks)
      • Resource controller creation can take a model as reference to automatically create a request and the handlers
    • Create middleware
    • Create request (validation, may be created alongside resource controllers)
    • Create model (+ its resource controller)
  • Database commands:
    • Run seeders
    • Run migrations (may open to a more advanced migration system)
    • Clear / recreate database
  • Miscellaneous:
    • Detailed routes list
    • Run tests (maybe not needed, as it would duplicate go test)
    • Generate OpenAPI specification #42
    • Open documentation page (goyave.dev)

Possible drawbacks

None.

Additional information

This issue is a feature proposal and is meant to be discussed. If you have any other idea for this CLI Utility, feel free to suggest it!
It is also a good candidate if you want to contribute to the project.
This should probably be developed as a side-project.

Validation error message for type-dependent rules when field is not in body

Bug report

Version: 2.10.2
Environment: Ubuntu 20.04, Go 1.14.6
Reproduces: always

Validation error: "validation.rules.size.unsupported" when a field is validated with type-dependent rule and it's entirely absent from the request body.

Expected result:

The message shouldn't be shown or should use a generic message.

How to reproduce

With the following rule set:

var (
	TestRequest validation.RuleSet = validation.RuleSet{
		"test": {"required", "string", "size:2"},
	}
)

Send a request with the following body:

{}

Response is:

{
    "validationError": {
        "test": [
            "The test is required.",
            "The test must be a string.",
            "validation.rules.size.unsupported"
        ]
    }
}

Route parameter converter

Proposal

Route parameter converters are functions converting route parameters into another type of data, typically models. The main use-case would be to create a built-in url converter for models removing parameter int conversion and SQL query from the controller.

For example, for the following route definition:

GET /user/{User}

The route parameter converter would take the "User" route parameter and search for the "User" registered model based on its primary key (defined by the gorm tag) in database. If no record is found, then the route will not match and a 404 error will be returned.

This feature could be implemented in a middleware or in the core of the router. However, for performance reasons, a middleware called after the route matched with the current simple matching process would be preferred. That would avoid a lot of unnecessary SQL queries.

Developers using the framework should be able to implement route parameter converters. How to define which converter to use when defining a route is a question that remains open.

Possible drawbacks

It may be difficult to handle multi-parameter routes and especially relations between models. For example, the following route is retrieving a forum post from the author "User":

GET /user/{User}/post/{Post}

How could we check and guarantee that the Post is related to User? Should we use the gorm field tags? What if there are multiple foreign keys to the same table?

An expensive work of reflection may be needed for this kind of use-case.

Additional information

This issue is a feature proposal and is meant to be discussed.
It is also a good candidate if you want to contribute to the project.

Documentation on Gorm migrations incorrect

Description

The Goyave summary of Gorm's migration is incorrect. Gorm states that tables, columns, and indexes are create. Goyave states that columns and indexes won't be created.

From the Gorm documentation:

AutoMigrate will ONLY create tables, missing columns and missing indexes, and WON’T change existing column’s type or delete unused columns to protect your data.

As written in Goyave:

Automatic migrations only creates tables. Missing columns and indexes won't be created, modified columns won't be changed and unused columns won't be deleted.

Support JWT custom claims

Proposal

I propose to support custom claims in the generated JWT token.

The user fields to be included as claims could be specified by adding auth:"jwtInclude" (to follow current auth prefix).

This would allow people putting custom information in the token, e.g. when thinking about username, some additional name fields or permissions (I saw you're planning support in the future).

Possible drawbacks

By default - none, if someone uses too many fields then the token will be heavy, but that's up to user of the framework.

Additional information

I think I can handle this contribution if you agree.

Authenticator: Ability to define a function executed if auth failed

Proposal

With the current implementation of auth.Authenticator and auth.Middleware, it is not possible to add any custom behavior if the authentication failed. It is not possible to use status handler neither because the middleware is writing to the response.

Suggested solution: add an interface that authenticators can implement. For example:

type Foo interface{
    OnUnauthorized(error)
}

If the authenticator used in auth.Middleware implements this interface, then call OnUnauthorized() if Authenticate() doesn't return nil.

Possible drawbacks

None.

CORS options not working on subrouters

Bug report

Version: v3.10.0
Environment: Ubuntu 20.04, Go 1.16.2
Reproduces: always

The CORS options used by requestHandler is always the one from the main router, meaning CORS options defined on subrouters won't work as expected.

Value null after ToStruct and validation ruleset

Bug report

Version: 3.7.1
Environment: Ubuntu 20.04
Reproduces:

For the Given struct :

type MyStruct struct {
	gorm.Model
	ID        uint `gorm:"primarykey"`
	PosX     float64
	PosY      float64
	PosZ      float64
}

How it occurred

  1. Validation
"pos_x":     {"required", "numeric"},
"pos_y":     {"required", "numeric"},
"pos_z":     {"required", "numeric"},
  1. On a POST request
pos_x: -970.4351,
pos_y: -664.2330,
pos_z: 44.2087,
  1. ToStruct
random := model.MyStruct{}
if err := request.ToStruct(&random); err != nil {
        panic(err)
}
  1. Values
fmt.Print(random.PosX)
// 0.000000

Expected result:

fmt.Print(random.PosX)
// -970.4351

Additional information

Problem from ToStruct() ?
Naming convention in POST request : pos_x -> PosX ?

Laravel

Is this loosly based on PHP Laravel framework?

Native middleware don't use replaced http.Request

Bug report

Version: v3.10.0
Environment: Ubuntu 20.04, Go 1.16
Reproduces: always

goyave.NativeMiddleware doesn't work properly with middleware replacing the http.Request (via request.WithContext() for example).

Suggested fix: Check if the pointer to the http.Request is the same as the original one. If that is not the case, simply replace it.

JSON body request example

Support

Version: v2.10.0

Can you please provide an example of a JSON body request controller with validation?

Add request context to validation

Hello, firstly thank you for this project.

Proposal

Add the context goyave.Request in the validation.

This gives the possibility to create custom validations with parameters from the Request like the user.

Current custom validation from the documentation:

func validateCustomFormat(field string, value interface{}, parameters []string, form map[string]interface{}) bool {
    str, ok := value.(string)

    if ok { // The data under validation is a string
        return regexp.MustCompile(parameters[0]).MatchString(str)
    }

    return false // Cannot validate this field
}

Proposal:

func validateUserIsAdmin(request *Goyave.Request, field string, value interface{}, parameters []string, form map[string]interface{}) bool {    
    if value != nil {
        user := request.User.(*model.User)

        userIsAdmin := user.isAdmin
    
        return userIsAdmin
    }
    
    return true
}

With the fields in Request.Extra this opens more possibility to create validations that match use cases.

Possible drawbacks

A changes in the organization of structures to avoid cyclical imports.

Asymmetric JWT signatures support

Proposal

Currently, the built-in JWT is using jwt.SigningMethodHS256. Using another method is not possible without re-implementing entirely the GenerateToken function and the JWTController. Adding support for customizable signing method, and more specifically, supporting asymmetric signing methods. That would enable federated or central authentication without leaking the JWT secret, which is an obvious security concern.

Implementation details can be discussed below.

Possible drawbacks

None.

Logging not working when no route matched

Version: v3.6.0
Environment: Ubuntu 20.04, Go 1.15.6
Reproduces: always

This is not really a bug. Requests that don't match any route (404 or 405) are not logged by the logging middleware, because middleware are only executed if a route is matching.

How it occurred

  1. Register the logging middleware
  2. Request a non-existing URL or an existing one with the wrong HTTP method
  3. Notice nothing is logged

Expected result:
Logging should probably log all requests, including 404 and 405.

How to reproduce

router.Middleware(log.CombinedLogMiddleware())

Social Media and Outreach Activities

Description

Goyave is a very powerful framework but it is not reaching many gophers because you have not been leveraging social media and other platforms to reach developers.

Possible drawbacks

Reach more Gophers and more contributors

Additional information

I am a beginner in Golang and would love to contribute to this framework by promoting it to gophers.

A real demo for benchmarks projects and to improve learning

Proposal

Your work on this project is amazing. I think it's the most accurate docs ever seen for a Go framework.

But I think it would also be nice to create a "real demo" repo both to learn even better the "best practices" for proper use and to use as a benchmark in the following projects:

And it's better for the author himself to use his framework for those benchmarks rather than someone who doesn't understand 100% how to best use it.

What do you think about it?

Thanks again for the wonderful work!

Content-Type with charset

Proposal

Current implementation automatically Unmarshal request data only in a situation when header Content-Type equals to application/json. RFC 7231 describing Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content specifies that Content-Type should contain media-type what is defined not only by data format (application/json) but also by additional metadata about processing (charset).

I'm not that experienced in Go, but I guess the problem is based on this check.

Some of the heavily used serialization libraries on Android – kotlinx.serialization, Moshi – put charset into Content-Type by default and it might look similar to this one: Content-Type: application/json; charset=utf-8

To summarize, with this curl request, content of request.Data is unmarshalled automatically:

curl -X "POST" "http://localhost:8080/v1/login" \
     -H 'Content-Type: application/json' \
     -d $'{
  "email": "[email protected]",
  "password": "12345678"
}'

Whereas with this request not:

curl -X "POST" "http://localhost:8080/v1/login" \
     -H 'Content-Type: application/json; charset=utf-8' \
     -d $'{
  "email": "[email protected]",
  "password": "12345678"
}'

Possible drawbacks

I'm not sure if it's sufficient just change mentioned check from == (equals) into contains() or it is necessary to take provided charset into account when data are unmarshalled.

Unable to set DisableForeignKeyConstraintWhenMigrating

Bug report

Version: 3.9.1
Environment: Mac OS, go1.15.2 darwin/amd64
Reproduces: always

Unable to set DisableForeignKeyConstraintWhenMigrating in gorm.Config

panic: constraints not implemented on sqlite, consider using DisableForeignKeyConstraintWhenMigrating, more details https://github.com/go-gorm/gorm/wiki/GORM-V2-Release-Note-Draft#all-new-migrator

Relevant logs:

SELECT count(*) FROM sqlite_master WHERE type = "table" AND tbl_name = "replies" AND (sql LIKE "%CONSTRAINT \"fk_replies_user\" %" OR sql LIKE "%CONSTRAINT fk_replies_user %" OR sql LIKE "%CONSTRAINT `fk_replies_user`%")
panic: constraints not implemented on sqlite, consider using DisableForeignKeyConstraintWhenMigrating, more details https://github.com/go-gorm/gorm/wiki/GORM-V2-Release-Note-Draft#all-new-migrator

goroutine 1 [running]:
goyave.dev/goyave/v3/database.Migrate()
        /Users/zaosoula/go/pkg/mod/goyave.dev/goyave/[email protected]/database/database.go:122 +0x11e
goyave.dev/goyave/v3.Start(0x46d3ae8, 0xc000143f68, 0x1)
        /Users/zaosoula/go/pkg/mod/goyave.dev/goyave/[email protected]/goyave.go:154 +0x11f
main.main()
        /Applications/MAMP/htdocs/projet-forum/api/main.go:24 +0x95
exit status 2
2021/06/22 09:29:47 exit status 1

Database models

type User struct {
	ID           int32  `gorm:"primary_key;AUTO_INCREMENT;column:id;type:integer;" json:"id" auth:"username"`
	Email        string `gorm:"column:email;type:varchar;" json:"email"`
	DisplayName  string `gorm:"column:displayName;type:varchar;" json:"displayName"`
	UserName     string `gorm:"column:userName;type:varchar;size:30;" json:"userName"`
	Password     string `gorm:"column:password;type:varchar;size:255;" json:"-"`
	LastLoginAt  int    `gorm:"column:lastLoginAt;type:integer;" json:"lastLoginAt"`
	RegisteredAt int    `gorm:"column:registeredAt;type:integer;" json:"registeredAt"`
	RefreshToken string `gorm:"column:refreshToken;type:varchar;size:1000;" json:"-"`
}

type Replies struct {
	ID     int32  `gorm:"primary_key;AUTO_INCREMENT;column:id;type:integer;" json:"id"`
	PostID int    `gorm:"column:postId;type:integer;" json:"postId"`
	Body   string `gorm:"column:body;type:varchar;" json:"body"`
	UserID int    `gorm:"column:userId;type:integer;" json:"userId"`
	User   User

	CreatedAt int `gorm:"column:createdAt;type:integer;" json:"createdAt"`
}

How it occurred

  1. Create an association (in this case a User in a Replies)
  2. Gorm throw an error asking to set DisableForeignKeyConstraintWhenMigrating to true (which seems not possible with Goyave

Expected result:
Be able to add properties to gorm.Config before the connection occurs

Additional information

The config is set in goyave/database/database.go, line 168

README of generated project to include /echo

Hi,

I am evaluating this framework, and based on the journey I would also try to contribute. πŸ˜ƒ

Just one remark for the moment: since the generated project includes - besides the /hello - also a /echo implementation, a cURL example how to call it might help also. Something like this:

  • a simple example:
    curl -G -w '\n' http://localhost:8080/echo -d text=abc123
    abc123
    $
  • an example when space encoding is needed:
    $ curl -G -w '\n' http://localhost:8080/echo --data-urlencode 'text=abc 123'
    abc 123
    $

Websocket

Proposal

Add support for websockets by building an adapter for gorilla's websocket library.

Possible drawbacks

None.

Additional information

This issue is a feature proposal and is meant to be discussed.
It is also a good candidate if you want to contribute to the project.

This could be built as a separate project.

response.Render status code

Bug report

Version: v2.10.2
Environment: Ubuntu 20.04, go 1.14.6
Reproduces: always

response.Render() writes status header before checking if template renders, preventing the return of 500 error in case of error.

Take this opportunity to add an example of usage with error handling to documentation.

Request extra-data or with context value

Proposal

It is currently not possible to add extra data to requests other than using request.Data or request.User. http.Request has WithContext(), which allows to add a context (WithValue()) to it.

Extra data in requests would allow middleware to process data that is not part of the request's body.

Implementation

The simplest solution would be to add a map[string]interface{} named Extra to the request structure. Initialize it empty to avoid memory usage.

Possible drawbacks

None.

Placeholders in language lines and pluralization

Proposal

Expand the placeholder system currently used in validation lines to be usable in any localized string. It could be possible to add support for pluralization at the same time: the ability to add a string somewhere in the language line depending on a parameter or the value of a placeholder.

This potential feature's target usage is not defined yet, feel free to suggest one.

Possible drawbacks

Depending on the implementation, using localization may have a little bit bigger impact on performances.

Additional information

This issue is a feature proposal and is meant to be discussed.
It is also a good candidate if you want to contribute to the project.

Request time tracking

Support

Version: v2.7.0

Hello there! I didn't find any events/hooks which i could use for tracking time and other things of request. I know that i can create middleware to do this, but is there a way to track time of the request?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.