diff --git a/.github/.editorconfig b/.github/.editorconfig deleted file mode 100644 index 85a920c7..00000000 --- a/.github/.editorconfig +++ /dev/null @@ -1,23 +0,0 @@ -; https://editorconfig.org/ - -root = true - -[*] -insert_final_newline = true -charset = utf-8 -trim_trailing_whitespace = true -indent_style = space -indent_size = 2 - -[{Makefile,go.mod,go.sum,*.go,.gitmodules}] -indent_style = tab -indent_size = 8 - -[*.md] -indent_size = 4 -trim_trailing_whitespace = false - -eclint_indent_style = unset - -[Dockerfile] -indent_size = 4 \ No newline at end of file diff --git a/.github/.hound.yml b/.github/.hound.yml deleted file mode 100644 index 59d7bb81..00000000 --- a/.github/.hound.yml +++ /dev/null @@ -1,2 +0,0 @@ -golint: - enabled: false \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 957d1b19..00000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @gofiber/maintainers diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md deleted file mode 100644 index 8c624059..00000000 --- a/.github/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,81 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our community include: - -- Demonstrating empathy and kindness toward other people -- Being respectful of differing opinions, viewpoints, and experiences -- Giving and gracefully accepting constructive feedback -- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -- Focusing on what is best not just for us as individuals, but for the overall community - -Examples of unacceptable behavior include: - -- The use of sexualized language or imagery, and sexual attention or advances of any kind -- Trolling, insulting or derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or email address, without their explicit permission -- Other conduct which could reasonably be considered inappropriate in a professional setting - -## Enforcement Responsibilities - -Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. - -Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [Gitter](https://gitter.im/gofiber/community). All complaints will be reviewed and investigated promptly and fairly. - -All community leaders are obligated to respect the privacy and security of the reporter of any incident. - -## Enforcement Guidelines - -Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series of actions. - -**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within the community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, -available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. - -Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see the FAQ at -https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index 96e36872..00000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,28 +0,0 @@ -# Contributing - -When contributing to this repository, please first discuss the change you wish to make via our [Telegram](https://t.me/gofiber) group, by creating an [issue](https://github.com/gofiber/fiber/issues) or any other method with the owners of this repository before making a change. - -Please note: we have a [code of conduct](https://github.com/gofiber/fiber/blob/master/.github/CODE_OF_CONDUCT.md), please follow it in all your interactions with the `Fiber` project. - -## Pull Requests or Comits -Titles always we must use prefix according to below: - -> ๐Ÿ”ฅ Feature, โ™ป๏ธ Refactor, ๐Ÿฉน Fix, ๐Ÿšจ Test, ๐Ÿ“š Doc, ๐ŸŽจ Style -- ๐Ÿ”ฅ Feature: Add flow to add person -- โ™ป๏ธ Refactor: Rename file X to Y -- ๐Ÿฉน Fix: Improve flow -- ๐Ÿšจ Test: Validate to add a new person -- ๐Ÿ“š Doc: Translate to Portuguese middleware redirect -- ๐ŸŽจ Style: Respected pattern Golint - -All pull request that contains a feature or fix is mandatory to have unit tests. Your PR is only to be merged if you respect this flow. - -# ๐Ÿ‘ Contribute - -If you want to say **thank you** and/or support the active development of `Fiber`: - -1. Add a [GitHub Star](https://github.com/gofiber/fiber/stargazers) to the project. -2. Tweet about the project [on your Twitter](https://twitter.com/intent/tweet?text=%F0%9F%9A%80%20Fiber%20%E2%80%94%20is%20an%20Express.js%20inspired%20web%20framework%20build%20on%20Fasthttp%20for%20%23Go%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). -3. Write a review or tutorial on [Medium](https://medium.com/), [Dev.to](https://dev.to/) or personal blog. -4. Help us to translate our API Documentation via [Crowdin](https://crowdin.com/project/gofiber) [![Crowdin](https://badges.crowdin.net/gofiber/localized.svg)](https://crowdin.com/project/gofiber) -5. Support the project by donating a [cup of coffee](https://buymeacoff.ee/fenny). \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index d56a2a1a..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,8 +0,0 @@ -# These are supported funding model platforms - -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -custom: https://gofiber.io/support diff --git a/.github/ISSUE_TEMPLATE/---bug.md b/.github/ISSUE_TEMPLATE/---bug.md deleted file mode 100644 index bfcf951f..00000000 --- a/.github/ISSUE_TEMPLATE/---bug.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: "\U0001F41B Bug" -about: Create a report to help us improve -title: "\U0001F41B " -labels: 'Type: Bug' -assignees: '' - ---- - -**Fiber version** - -**Issue description** - -**Code snippet** - -```go -package main - -import "github.com/gofiber/fiber" - -func main() { - app := fiber.New() - - // Steps to reproduce - - app.Listen(3000) -} -``` diff --git a/.github/ISSUE_TEMPLATE/---feature.md b/.github/ISSUE_TEMPLATE/---feature.md deleted file mode 100644 index c3baf503..00000000 --- a/.github/ISSUE_TEMPLATE/---feature.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: "\U0001F680 Feature" -about: Suggest an idea for this project -title: "\U0001F680 " -labels: 'Type: Feature' -assignees: '' - ---- - -**Is your feature request related to a problem?** - -**Describe the solution you'd like** - -**Describe alternatives you've considered** - -**Additional context** diff --git a/.github/ISSUE_TEMPLATE/---question.md b/.github/ISSUE_TEMPLATE/---question.md deleted file mode 100644 index 70ffe385..00000000 --- a/.github/ISSUE_TEMPLATE/---question.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: "\U0001F917 Question" -about: Ask a question so we can help -title: "\U0001F917 " -labels: 'Type: Question' -assignees: '' - ---- - -**Question description** - -**Code snippet** _Optional_ - -```go -package main - -import "github.com/gofiber/fiber" - -func main() { - app := fiber.New() - // .. -} -``` diff --git a/.github/README.md b/.github/README.md deleted file mode 100644 index 5f06e94f..00000000 --- a/.github/README.md +++ /dev/null @@ -1,592 +0,0 @@ -

- - Fiber - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -

-

- Fiber is an Express inspired web framework built on top of Fasthttp, the fastest HTTP engine for Go. Designed to ease things up for fast development with zero memory allocation and performance in mind. -

- -## โšก๏ธ Quickstart - -```go -package main - -import "github.com/gofiber/fiber" - -func main() { - app := fiber.New() - - app.Get("/", func(c *fiber.Ctx) { - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -## ๐Ÿค– Benchmarks - -These tests are performed by [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) and [Go Web](https://github.com/smallnest/go-web-framework-benchmark). If you want to see all results, please visit our [Wiki](https://docs.gofiber.io/benchmarks). - -

- - -

- -## โš™๏ธ Installation - -First of all, [download](https://golang.org/dl/) and install Go. `1.11` or higher is required. - -Installation is done using the [`go get`](https://golang.org/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) command: - -```bash -go get -u github.com/gofiber/fiber -``` - -## ๐ŸŽฏ Features - -- Robust [routing](https://docs.gofiber.io/routing) -- Serve [static files](https://docs.gofiber.io/application#static) -- Extreme [performance](https://docs.gofiber.io/benchmarks) -- [Low memory](https://docs.gofiber.io/benchmarks) footprint -- [API endpoints](https://docs.gofiber.io/context) -- [Middleware](https://docs.gofiber.io/middleware) & [Next](https://docs.gofiber.io/context#next) support -- [Rapid](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) server-side programming -- [Template engines](https://github.com/gofiber/template) -- [WebSocket support](https://docs.gofiber.io/middleware#websocket) -- [Rate Limiter](https://docs.gofiber.io/middleware#limiter) -- Translated in [15 languages](https://docs.gofiber.io/) -- And much more, [explore Fiber](https://docs.gofiber.io/) - -## ๐Ÿ’ก Philosophy - -New gophers that make the switch from [Node.js](https://nodejs.org/en/about/) to [Go](https://golang.org/doc/) are dealing with a learning curve before they can start building their web applications or microservices. Fiber, as a **web framework**, was created with the idea of **minimalism** and follows the **UNIX way**, so that new gophers can quickly enter the world of Go with a warm and trusted welcome. - -Fiber is **inspired** by Express, the most popular web framework on the Internet. We combined the **ease** of Express and **raw performance** of Go. If you have ever implemented a web application in Node.js (_using Express or similar_), then many methods and principles will seem **very common** to you. - -We **listen** to our users in [issues](https://github.com/gofiber/fiber/issues), Discord [channel](https://gofiber.io/discord) _and all over the Internet_ to create a **fast**, **flexible** and **friendly** Go web framework for **any** task, **deadline** and developer **skill**! Just like Express does in the JavaScript world. - -## ๐Ÿ‘€ Examples - -Listed below are some of the common examples. If you want to see more code examples , please visit our [Recipes repository](https://github.com/gofiber/recipes) or visit our hosted [API documentation](https://docs.gofiber.io). - -#### ๐Ÿ“– [**Basic Routing**](https://docs.gofiber.io/#basic-routing) - -```go -func main() { - app := fiber.New() - - // GET /john - app.Get("/:name", func(c *fiber.Ctx) { - msg := fmt.Sprintf("Hello, %s ๐Ÿ‘‹!", c.Params("name")) - c.Send(msg) // => Hello john ๐Ÿ‘‹! - }) - - // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ‘ด %s is %s years old", c.Params("name"), c.Params("age")) - c.Send(msg) // => ๐Ÿ‘ด john is 75 years old - }) - - // GET /dictionary.txt - app.Get("/:file.:ext", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ“ƒ %s.%s", c.Params("file"), c.Params("ext")) - c.Send(msg) // => ๐Ÿ“ƒ dictionary.txt - }) - - // GET /flights/LAX-SFO - app.Get("/flights/:from-:to", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ’ธ From: %s, To: %s", c.Params("from"), c.Params("to")) - c.Send(msg) // => ๐Ÿ’ธ From: LAX, To: SFO - }) - - // GET /api/register - app.Get("/api/*", func(c *fiber.Ctx) { - msg := fmt.Sprintf("โœ‹ %s", c.Params("*")) - c.Send(msg) // => โœ‹ /api/register - }) - - app.Listen(3000) -} -``` - -#### ๐Ÿ“– [**Serving Static Files**](https://docs.gofiber.io/application#static) - -```go -func main() { - app := fiber.New() - - app.Static("/", "./public") - // => http://localhost:3000/js/script.js - // => http://localhost:3000/css/style.css - - app.Static("/prefix", "./public") - // => http://localhost:3000/prefix/js/script.js - // => http://localhost:3000/prefix/css/style.css - - app.Static("*", "./public/index.html") - // => http://localhost:3000/any/path/shows/index/html - - app.Listen(3000) -} -``` - -#### ๐Ÿ“– [**Middleware & Next**](https://docs.gofiber.io/context#next) - -```go -func main() { - app := fiber.New() - - // Match any route - app.Use(func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‡ First handler") - c.Next() - }) - - // Match all routes starting with /api - app.Use("/api", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅˆ Second handler") - c.Next() - }) - - // GET /api/register - app.Get("/api/list", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‰ Last handler") - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -
- ๐Ÿ“š Show more code examples - -### Views engines - -๐Ÿ“– [Settings](https://docs.gofiber.io/application#settings) -๐Ÿ“– [Engines](https://github.com/gofiber/template) -๐Ÿ“– [Render](https://docs.gofiber.io/context#render) - -Fiber defaults to the [html/template](https://golang.org/pkg/html/template/) when no view engine is set. - -If you want to execute partials or use a different engine like [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) or [pug](https://github.com/Joker/jade) etc.. - -Checkout our [Template](https://github.com/gofiber/template) package that support multiple view engines. - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/template/pug" -) - -func main() { - // You can setup Views engine before initiation app: - app := fiber.New(&fiber.Settings{ - Views: pug.New("./views", ".pug"), - }) - - // OR after initiation app at any convenient location: - app.Settings.Views = pug.New("./views", ".pug"), - - // And now, you can call template `./views/home.pug` like this: - app.Get("/", func(c *fiber.Ctx) { - c.Render("home", fiber.Map{ - "title": "Homepage", - "year": 1999, - }) - }) - - // ... -} -``` - -### Grouping routes into chains - -๐Ÿ“– [Group](https://docs.gofiber.io/application#group) - -```go -func main() { - app := fiber.New() - - // Root API route - api := app.Group("/api", cors()) // /api - - // API v1 routes - v1 := api.Group("/v1", mysql()) // /api/v1 - v1.Get("/list", handler) // /api/v1/list - v1.Get("/user", handler) // /api/v1/user - - // API v2 routes - v2 := api.Group("/v2", mongodb()) // /api/v2 - v2.Get("/list", handler) // /api/v2/list - v2.Get("/user", handler) // /api/v2/user - - // ... -} -``` - -### Middleware logger - -๐Ÿ“– [Logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - // Default - app.Use(middleware.Logger()) - - // Custom logging format - app.Use(middleware.Logger("${method} - ${path}")) - - // Custom Config - app.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ - Next: func(ctx *fiber.Ctx) bool { - return ctx.Path() != "/private" - }, - Format: "${method} - ${path}", - Output: io.Writer, - })) - - app.Listen(3000) -} -`````` -### Cross-Origin Resource Sharing (CORS) - -๐Ÿ“– [CORS](https://docs.gofiber.io/middleware#cors) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/cors" -) - -func main() { - app := fiber.New() - - // CORS with default config - app.Use(cors.New()) - - app.Listen(3000) -} -``` - -Check CORS by passing any domain in `Origin` header: - -```bash -curl -H "Origin: http://example.com" --verbose http://localhost:3000 -``` - -### Custom 404 response - -๐Ÿ“– [HTTP Methods](https://docs.gofiber.io/application#http-methods) - -```go -func main() { - app := fiber.New() - - app.Static("./public") - - app.Get("/demo", func(c *fiber.Ctx) { - c.Send("This is a demo!") - }) - - app.Post("/register", func(c *fiber.Ctx) { - c.Send("Welcome!") - }) - - // Last middleware to match anything - app.Use(func(c *fiber.Ctx) { - c.SendStatus(404) - // => 404 "Not Found" - }) - - app.Listen(3000) -} -``` - -### JSON Response - -๐Ÿ“– [JSON](https://docs.gofiber.io/context#json) - -```go -type User struct { - Name string `json:"name"` - Age int `json:"age"` -} - -func main() { - app := fiber.New() - - app.Get("/user", func(c *fiber.Ctx) { - c.JSON(&User{"John", 20}) - // => {"name":"John", "age":20} - }) - - app.Get("/json", func(c *fiber.Ctx) { - c.JSON(fiber.Map{ - "success": true, - "message": "Hi John!", - }) - // => {"success":true, "message":"Hi John!"} - }) - - app.Listen(3000) -} -``` - -### WebSocket Upgrade - -๐Ÿ“– [Websocket](https://docs.gofiber.io/middleware#websocket) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/websocket" -) - -func main() { - app := fiber.New() - - app.Get("/ws", websocket.New(func(c *websocket.Conn) { - for { - mt, msg, err := c.ReadMessage() - if err != nil { - log.Println("read:", err) - break - } - log.Printf("recv: %s", msg) - err = c.WriteMessage(mt, msg) - if err != nil { - log.Println("write:", err) - break - } - } - })) - - app.Listen(3000) - // ws://localhost:3000/ws -} -``` - -### Recover middleware - -๐Ÿ“– [Recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - app.Use(middleware.Recover()) - - app.Get("/", func(c *fiber.Ctx) { - panic("normally this would crash your app") - }) - - app.Listen(3000) -} -``` -
- -## ๐Ÿงฌ Fiber Middleware - -The Fiber middleware modules listed here are maintained by the [Fiber team](https://github.com/orgs/gofiber/people). - -| Middleware | Description | Built-in middleware | -| :--- | :--- | :--- | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | - | -| [basicauth](https://github.com/gofiber/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | - | -| [compress](https://github.com/gofiber/fiber/blob/master/middleware/compress.md) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | `middleware.Compress()` | -| [cors](https://github.com/gofiber/cors) | Enable cross-origin resource sharing \(CORS\) with various options. | - | -| [csrf](https://github.com/gofiber/csrf) | Protect from CSRF exploits. | - | -| [filesystem](https://github.com/gofiber/fiber/blob/master/middleware/filesystem.md) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | - | -| [favicon](https://github.com/gofiber/fiber/blob/master/middleware/favicon.md) | Ignore favicon from logs or serve from memory if a file path is provided. | `middleware.Favicon()` | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | - | -| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | - | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | - | -| [limiter](https://github.com/gofiber/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | - | -| [logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) | HTTP request/response logger. | `middleware.Logger()` | -| [pprof](https://github.com/gofiber/pprof) | Special thanks to Matthew Lee \(@mthli\) | - | -| [recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) | Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized[ ErrorHandler](error-handling.md). | `middleware.Recover()` | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | - | -| [requestid](https://github.com/gofiber/fiber/blob/master/middleware/request_id.md) | Request ID middleware generates a unique id for a request. | `middleware.RequestID()` | -| [session](https://github.com/gofiber/session) | This session middleware is build on top of fasthttp/session by @savsgio MIT. Special thanks to @thomasvvugt for helping with this middleware. | - | -| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | - | -| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | - | - -## ๐ŸŒฑ Third Party Middlewares - -This is a list of middlewares that are created by the Fiber community, please create a PR if you want to see yours! -- [arsmn/fiber-casbin](https://github.com/arsmn/fiber-casbin) -- [arsmn/fiber-introspect](https://github.com/arsmn/fiber-introspect) -- [arsmn/fiber-swagger](https://github.com/arsmn/fiber-swagger) -- [arsmn/gqlgen](https://github.com/arsmn/gqlgen) -- [codemicro/fiber-cache](https://github.com/codemicro/fiber-cache) -- [itsursujit/fiber-boilerplate](https://github.com/itsursujit/fiber-boilerplate) -- [juandiii/go-jwk-security](https://github.com/juandiii/go-jwk-security) -- [kiyonlin/fiber_limiter](https://github.com/kiyonlin/fiber_limiter) -- [shareed2k/fiber_limiter](https://github.com/shareed2k/fiber_limiter) -- [shareed2k/fiber_tracing](https://github.com/shareed2k/fiber_tracing) -- [thomasvvugt/fiber-boilerplate](https://github.com/thomasvvugt/fiber-boilerplate) - -## ๐Ÿ’ฌ Media - -

- - -

- -- [Welcome to Fiber โ€” an Express.js styled web framework written in Go with โค๏ธ](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) โ€” _03 Feb 2020_ -- [Fiber released v1.7! ๐ŸŽ‰ What's new and is it still fast, flexible and friendly?](https://dev.to/koddr/fiber-v2-is-out-now-what-s-new-and-is-he-still-fast-flexible-and-friendly-3ipf) โ€” _21 Feb 2020_ -- [๐Ÿš€ Fiber v1.8. What's new, updated and re-thinked?](https://dev.to/koddr/fiber-v1-8-what-s-new-updated-and-re-thinked-339h) โ€” _03 Mar 2020_ -- [Is switching from Express to Fiber worth it? ๐Ÿค”](https://dev.to/koddr/are-sure-what-your-lovely-web-framework-running-so-fast-2jl1) โ€” _01 Apr 2020_ -- [Creating Fast APIs In Go Using Fiber](https://dev.to/jozsefsallai/creating-fast-apis-in-go-using-fiber-59m9) โ€” _07 Apr 2020_ -- [Building a Basic REST API in Go using Fiber](https://tutorialedge.net/golang/basic-rest-api-go-fiber/) - _23 Apr 2020_ -- [๐Ÿ“บ Building a REST API using GORM and Fiber](https://youtu.be/Iq2qT0fRhAA) - _25 Apr 2020_ -- [๐ŸŒŽ Create a travel list app with Go, Fiber, Angular, MongoDB and Google Cloud Secret Manager](https://blog.yongweilun.me/create-a-travel-list-app-with-go-fiber-angular-mongodb-and-google-cloud-secret-manager-ck9fgxy0p061pcss1xt1ubu8t) - _25 Apr 2020_ -- [Fiber v1.9.6 ๐Ÿ”ฅ How to improve performance by 817% and stay fast, flexible and friendly?](https://dev.to/koddr/fiber-v1-9-5-how-to-improve-performance-by-817-and-stay-fast-flexible-and-friendly-2dp6) - _12 May 2020_ -- [The road to web-based authentication with Fiber โšก](https://vugt.me/the-road-to-web-based-authentication-with-fiber/) - _20 May 2020_ -- [Building an Express-style API in Go with Fiber](https://blog.logrocket.com/express-style-api-go-fiber/) - _10 June 2020_ -- [ๅŸบไบŽgolang fiberๅ’Œangularๅผ€ๅ‘web](https://zhuanlan.zhihu.com/p/148925642) - _19 June 2020_ -- [ๅŸบไบŽๅปถ่ฟŸ่ฎก็ฎ—ไปค็‰Œๆกถ็š„gofiber้ข‘็Ž‡้™ๅˆถไธญ้—ดไปถๅฎž็Žฐ](https://zhuanlan.zhihu.com/p/149308936) - _20 June 2020_ -- [Construir una API en Golang con Fiber ๐Ÿ‡ช๐Ÿ‡ธ](https://enbonnet.me/article/53/construir-api-golang-con-fiber) - _28 June 2020_ -- [๐Ÿ“บWhy Go Fiber Is THE New Framework To Learn](https://www.youtube.com/watch?v=kvwsPeWDLM8) - _29 June 2020_ -- [่งฃๆžGofiber่ทฏ็”ฑ็ฎก็†](https://zhuanlan.zhihu.com/p/152494502) - _08 July 2020_ -- [๐Ÿ“บ Introduction to Fiber - An Express-inspired web framework](https://youtu.be/MfFi4Gt-tos) - _25 July 2020_ -- [๐Ÿ“บ How to use the Go Fiber JWT middleware](https://youtu.be/ejEizICXm9w) - _30 July 2020_ -- [๐Ÿ“บ Introduction to SQLC and how to use it with Fiber](https://youtu.be/xIqulnDQwp0) - _11 Aug 2020_ - -## ๐Ÿ‘ Contribute - -If you want to say **thank you** and/or support the active development of `Fiber`: - -1. Add a [GitHub Star](https://github.com/gofiber/fiber/stargazers) to the project. -2. Tweet about the project [on your Twitter](https://twitter.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). -3. Write a review or tutorial on [Medium](https://medium.com/), [Dev.to](https://dev.to/) or personal blog. -4. Help us to translate our API Documentation via [Crowdin](https://crowdin.com/project/gofiber) [![Crowdin](https://badges.crowdin.net/gofiber/localized.svg)](https://crowdin.com/project/gofiber) -5. Support the project by donating a [cup of coffee](https://buymeacoff.ee/fenny). - -## โ˜• Supporters - -Fiber is an open source project that runs on donations to pay the bills e.g. our domain name, gitbook, netlify and serverless hosting. If you want to support Fiber, you can โ˜• [**buy a coffee here**](https://buymeacoff.ee/fenny). - -| | User | Donation | -| :---------------------------------------------------------- | :----------------------------------------------- | :------- | -| ![](https://avatars.githubusercontent.com/u/204341?s=25 ) | [@destari](https://github.com/destari) | โ˜• x 10 | -| ![](https://avatars.githubusercontent.com/u/63164982?s=25 ) | [@dembygenesis](https://github.com/dembygenesis) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/56607882?s=25 ) | [@thomasvvugt](https://github.com/thomasvvugt) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/27820675?s=25 ) | [@hendratommy](https://github.com/hendratommy) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/1094221?s=25 ) | [@ekaputra07](https://github.com/ekaputra07) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/194590?s=25 ) | [@jorgefuertes](https://github.com/jorgefuertes) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/186637?s=25 ) | [@candidosales](https://github.com/candidosales) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/29659953?s=25 ) | [@l0nax](https://github.com/l0nax) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/59947262?s=25 ) | [@ankush](https://github.com/ankush) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/635852?s=25 ) | [@bihe](https://github.com/bihe) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/307334?s=25 ) | [@justdave](https://github.com/justdave) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/11155743?s=25 ) | [@koddr](https://github.com/koddr) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/29042462?s=25 ) | [@lapolinar](https://github.com/lapolinar) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/2978730?s=25 ) | [@diegowifi](https://github.com/diegowifi) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/44171355?s=25 ) | [@ssimk0](https://github.com/ssimk0) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/5638101?s=25 ) | [@raymayemir](https://github.com/raymayemir) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/619996?s=25 ) | [@melkorm](https://github.com/melkorm) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31022056?s=25 ) | [@marvinjwendt](https://github.com/thomasvvugt) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31921460?s=25 ) | [@toishy](https://github.com/toishy) | โ˜• x 1 | - -## โ€Žโ€๐Ÿ’ป Code Contributors - -Code Contributors - -## โญ๏ธ Stargazers - -Stargazers over time - -## โš ๏ธ License - -Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` is free and open-source software licensed under the [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Official logo was created by [Vic Shรณstak](https://github.com/koddr) and distributed under [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) license (CC BY-SA 4.0 International). - -**Third-party library licenses** -- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) -- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) -- [fasttemplate](https://github.com/valyala/fasttemplate/blob/master/LICENSE) -- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) diff --git a/.github/README_ar_SA.md b/.github/README_ar_SA.md deleted file mode 100644 index 1c7c0dc8..00000000 --- a/.github/README_ar_SA.md +++ /dev/null @@ -1,656 +0,0 @@ -

- - Fiber - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -

-

-

- Fiber ู‡ูˆ ุฅุทุงุฑ ูˆูŠุจ ู…ุณุชูˆุญู‰ ู…ู† Express ู…ุจู†ูŠ ุนู„ู‰ Fasthttp, ุงุณุฑุน ู…ุญุฑูƒ HTTP ู„ู€ Go. ู…ุตู…ู… ู„ูŠูƒูˆู† ุณู‡ู„ ู„ุฃุบุฑุงุถ ุงู„ุณุฑุนุฉ ู…ุน ุนุฏู… ุชุฎุตูŠุต ุฐุงูƒุฑุฉ ูˆุงู„ุฃุฏุงุก ูˆ ุงู„ุงุฏุงุก ุงู„ุนุงู„ูŠ ุฏุงุฆู…ุง. -
-

- -## โšก๏ธ ุจุฏุงูŠุฉ ุณุฑูŠุนุฉ - -
- - -```go -package main - -import "github.com/gofiber/fiber" - -func main() { - app := fiber.New() - - app.Get("/", func(c *fiber.Ctx) { - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - - -
- -## ๐Ÿค– ู…ู‚ุงูŠุณ ุงู„ุงุฏุงุก - -ูŠุชู… ุชู†ููŠุฐ ู‡ุฐู‡ ุงู„ุงุฎุชุจุงุฑุงุช ู…ู† ู‚ุจู„ [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) ูˆ [Go Web](https://github.com/smallnest/go-web-framework-benchmark). ุฅุฐุง ูƒู†ุช ุชุฑูŠุฏ ุฑุคูŠุฉ ุฌู…ูŠุน ุงู„ู†ุชุงุฆุฌ ุŒ ูŠุฑุฌู‰ ุฒูŠุงุฑุฉ ู…ูˆู‚ุนู†ุง [Wiki](https://docs.gofiber.io/benchmarks). - -

- - -

- - -## โš™๏ธ ุชุซุจูŠุช - -ู‚ุจู„ ูƒู„ ุดูŠ ู‚ู… , [ุจุชุญู…ูŠู„](https://golang.org/dl/) ูˆ ุชุซุจูŠุช Go. `1.11` ุฃูˆ ุฃุนู„ู‰ ู…ุทู„ูˆุจ. - -ุจุนุฏ ุงู„ุงู†ุชู‡ุงุก ู…ู† ุงู„ุชุซุจูŠุช ุงุณุชุฎุฏู… ุงู„ุงู…ุฑ [`go get`](https://golang.org/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) : - -
- - -```bash -go get -u github.com/gofiber/fiber -``` - -
- - -## ๐ŸŽฏ ุงู„ู…ูŠุฒุงุช - -- ู‚ูˆูŠ [routing](https://docs.gofiber.io/routing) -- ูŠู‚ุฏู… ุฎุฏู…ุฉ [static files](https://docs.gofiber.io/application#static) -- ุฃู‚ุตู‰ [ุฃุฏุงุก](https://docs.gofiber.io/benchmarks) -- [ุฐุงูƒุฑุฉ ู…ู†ุฎูุถุฉ](https://docs.gofiber.io/benchmarks) -- [API endpoints](https://docs.gofiber.io/context) -- [Middleware](https://docs.gofiber.io/middleware) & [Next](https://docs.gofiber.io/context#next) ู…ุฏุนูˆู… -- [ุณุฑูŠุน](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) server-side programming -- [Template engines](https://github.com/gofiber/template) -- [WebSocket ุฏุนู…](https://docs.gofiber.io/middleware#websocket) -- [Rate Limiter](https://docs.gofiber.io/middleware#limiter) -- ุชุฑุฌู… ุงู„ู‰ [15 ู„ุบุฉ ุฃุฎุฑู‰](https://docs.gofiber.io/) -- ูˆุฃูƒุซุฑ ุจูƒุซูŠุฑ, [ุงุณุชูƒุดู Fiber](https://docs.gofiber.io/) - -## ๐Ÿ’ก ูู„ุณูุฉ - -ู‚ูˆูุฑ(ู…ุณุชุฎุฏู…ูŠ ู„ุบุฉ Go ุงู„ุฌุฏุฏ) ุฌุฏูŠุฏ ูŠุฌุนู„ ุงู„ุชุจุฏูŠู„ ู…ู† [Node.js](https://nodejs.org/en/about/) ุงู„ู‰ [Go](https://golang.org/doc/)ุชุชุนุงู…ู„ ู…ุน ู…ู†ุญู†ู‰ ุงู„ุชุนู„ู… ู‚ุจู„ ุฃู† ูŠุชู…ูƒู†ูˆุง ู…ู† ุงู„ุจุฏุก ููŠ ุจู†ุงุกุชุทุจูŠู‚ุงุช ุงู„ูˆูŠุจ . Fiber, ูƒู€ **ุฅุทุงุฑ ุงู„ูˆูŠุจ**, ุชู… ุฅู†ุดุงุคู‡ ุจููƒุฑุฉ **minimalism** ูˆูŠุชุจุน **UNIX way**, ุญุชู‰ ูŠุชู…ูƒู† ุงู„ู‚ูˆูุฑูˆู† ุงู„ุฌุฏุฏ ู…ู† ุฏุฎูˆู„ ุนุงู„ู… Go ุจุชุฑุญูŠุจ ุญุงุฑ ูˆู…ูˆุซูˆู‚. - -Fiber ู‡ูˆ **ู…ุณุชูˆุญู‰** ู…ู† Express, ุฅุทุงุฑ ุงู„ูˆูŠุจ ุงู„ุฃูƒุซุฑ ุดุนุจูŠุฉ ุนู„ู‰ ุงู„ุฅู†ุชุฑู†ุช. ู‚ู…ู†ุง ุจุฏู…ุฌ **ุณู‡ูˆู„ุฉ** ุงู„ู€ Express ูˆ **ุงู„ุฃุฏุงุก ุงู„ุฎุงู…** ู„ู€ Go. ุฅุฐุง ูƒู†ุช ู‚ุฏ ู‚ู…ุช ุจุชุทุจูŠู‚ ุชุทุจูŠู‚ ูˆูŠุจ ููŠ Node.js (_using Express or similar_), ุณุชุธู‡ุฑ ุงู„ุนุฏูŠุฏ ู…ู† ุงู„ุฃุณุงู„ูŠุจ ูˆุงู„ู…ุจุงุฏุฆ **ุงู„ุงูƒุซุฑ ุดูŠูˆุนุงู‹** ู„ูƒ. - -ู†ุญู† **ู†ุตุบูŠ** ู„ู…ุณุชุฎุฏู…ูŠู†ุง [issues](https://github.com/gofiber/fiber/issues), ู†ู†ุงู‚ุด [channel](https://gofiber.io/discord) _ูˆููŠ ุฌู…ูŠุน ุฃู†ุญุงุก ุงู„ุฅู†ุชุฑู†ุช_ ู„ุฅู†ุดุงุก **ุณุฑูŠุน**, **ู…ุฑู†** ูˆ **ู…ุฃู„ูˆู** Go ุฅุทุงุฑ ุงู„ูˆูŠุจ ู„ู€ **ู„ุฃูŠ** ู…ู‡ู…ุฉ, **ุงู„ู…ูˆุนุฏ ุงู„ุฃุฎูŠุฑ -** ูˆ ุชุทูˆูŠุฑ **ู…ู‡ุงุฑุงุช**! ูู‚ุท ู…ุซู„ Express ุชูุนู„ ู„ู€ JavaScript ุนุงู„ู…. - -## ๐Ÿ‘€ ุฃู…ุซู„ุฉ - -ููŠู…ุง ูŠู„ูŠ ุจุนุถ ุงู„ุฃู…ุซู„ุฉ ุงู„ุดุงุฆุนุฉ. ุฅุฐุง ูƒู†ุช ุชุฑุบุจ ููŠ ุฑุคูŠุฉ ุงู„ู…ุฒูŠุฏ ู…ู† ุฃู…ุซู„ุฉ ุงู„ุชุนู„ูŠู…ุงุช ุงู„ุจุฑู…ุฌูŠุฉ, ูŠุฑุฌู‰ ุฒูŠุงุฑุฉ [Recipes repository](https://github.com/gofiber/recipes) ุงูˆ ุฒูŠุงุฑุฉ [API documentation](https://docs.gofiber.io). - -#### ๐Ÿ“– [**Basic Routing**](https://docs.gofiber.io/#basic-routing) - -
- -```go -func main() { - app := fiber.New() - - // GET /john - app.Get("/:name", func(c *fiber.Ctx) { - msg := fmt.Sprintf("Hello, %s ๐Ÿ‘‹!", c.Params("name")) - c.Send(msg) // => Hello john ๐Ÿ‘‹! - }) - - // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ‘ด %s is %s years old", c.Params("name"), c.Params("age")) - c.Send(msg) // => ๐Ÿ‘ด john is 75 years old - }) - - // GET /dictionary.txt - app.Get("/:file.:ext", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ“ƒ %s.%s", c.Params("file"), c.Params("ext")) - c.Send(msg) // => ๐Ÿ“ƒ dictionary.txt - }) - - // GET /flights/LAX-SFO - app.Get("/flights/:from-:to", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ’ธ From: %s, To: %s", c.Params("from"), c.Params("to")) - c.Send(msg) // => ๐Ÿ’ธ From: LAX, To: SFO - }) - - // GET /api/register - app.Get("/api/*", func(c *fiber.Ctx) { - msg := fmt.Sprintf("โœ‹ %s", c.Params("*")) - c.Send(msg) // => โœ‹ /api/register - }) - - app.Listen(3000) -} -``` - -
- -#### ๐Ÿ“– [**Serving Static Files**](https://docs.gofiber.io/application#static) - -
- -```go -func main() { - app := fiber.New() - - app.Static("/", "./public") - // => http://localhost:3000/js/script.js - // => http://localhost:3000/css/style.css - - app.Static("/prefix", "./public") - // => http://localhost:3000/prefix/js/script.js - // => http://localhost:3000/prefix/css/style.css - - app.Static("*", "./public/index.html") - // => http://localhost:3000/any/path/shows/index/html - - app.Listen(3000) -} -``` -
- -#### ๐Ÿ“– [**Middleware & Next**](https://docs.gofiber.io/context#next) - -
- -```go -func main() { - app := fiber.New() - - // Match any route - app.Use(func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‡ First handler") - c.Next() - }) - - // Match all routes starting with /api - app.Use("/api", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅˆ Second handler") - c.Next() - }) - - // GET /api/register - app.Get("/api/list", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‰ Last handler") - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -
- -
- ๐Ÿ“š ุฅุธู‡ุงุฑ ุงู„ู…ุฒูŠุฏ ู…ู† ุฃู…ุซู„ุฉ ุงู„ุชุนู„ูŠู…ุงุช ุงู„ุจุฑู…ุฌูŠุฉ - -### Views engines - -๐Ÿ“– [Settings](https://docs.gofiber.io/application#settings) -๐Ÿ“– [Engines](https://github.com/gofiber/template) -๐Ÿ“– [Render](https://docs.gofiber.io/context#render) - -Fiber defaults to the [html/template](https://golang.org/pkg/html/template/) when no view engine is set. - -If you want to execute partials or use a different engine like [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) or [pug](https://github.com/Joker/jade) etc.. - -Checkout our [Template](https://github.com/gofiber/template) package that support multiple view engines. - - -
- -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/template/pug" -) - -func main() { - // You can setup Views engine before initiation app: - app := fiber.New(&fiber.Settings{ - Views: pug.New("./views", ".pug"), - }) - - // OR after initiation app at any convenient location: - app.Settings.Views = pug.New("./views", ".pug"), - - // And now, you can call template `./views/home.pug` like this: - app.Get("/", func(c *fiber.Ctx) { - c.Render("home", fiber.Map{ - "title": "Homepage", - "year": 1999, - }) - }) - - // ... -} -``` - -
- -### Grouping routes into chains - -๐Ÿ“– [Group](https://docs.gofiber.io/application#group) - -
- -```go -func main() { - app := fiber.New() - - // Root API route - api := app.Group("/api", cors()) // /api - - // API v1 routes - v1 := api.Group("/v1", mysql()) // /api/v1 - v1.Get("/list", handler) // /api/v1/list - v1.Get("/user", handler) // /api/v1/user - - // API v2 routes - v2 := api.Group("/v2", mongodb()) // /api/v2 - v2.Get("/list", handler) // /api/v2/list - v2.Get("/user", handler) // /api/v2/user - - // ... -} -``` - -
- -### Middleware logger - -๐Ÿ“– [Logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) - -
- -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - // Default - app.Use(middleware.Logger()) - - // Custom logging format - app.Use(middleware.Logger("${method} - ${path}")) - - // Custom Config - app.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ - Next: func(ctx *fiber.Ctx) bool { - return ctx.Path() != "/private" - }, - Format: "${method} - ${path}", - Output: io.Writer, - })) - - app.Listen(3000) -} -`````` - -
- -### Cross-Origin Resource Sharing (CORS) - -๐Ÿ“– [CORS](https://docs.gofiber.io/middleware#cors) - -
- -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/cors" -) - -func main() { - app := fiber.New() - - // CORS with default config - app.Use(cors.New()) - - app.Listen(3000) -} -``` - -
- -ุงู„ุชุญู‚ู‚ ู…ู† CORS ุนู† ุทุฑูŠู‚ ุชู…ุฑูŠุฑ ุฃูŠ ู…ุฌุงู„ `Origin` ุงู„ุนู†ูˆุงู†: - -
- -```bash -curl -H "Origin: http://example.com" --verbose http://localhost:3000 -``` -
- - -### ู…ุฎุตุต 404 response - -๐Ÿ“– [HTTP Methods](https://docs.gofiber.io/application#http-methods) - - -
- -```go -func main() { - app := fiber.New() - - app.Static("./public") - - app.Get("/demo", func(c *fiber.Ctx) { - c.Send("This is a demo!") - }) - - app.Post("/register", func(c *fiber.Ctx) { - c.Send("Welcome!") - }) - - // Last middleware to match anything - app.Use(func(c *fiber.Ctx) { - c.SendStatus(404) - // => 404 "Not Found" - }) - - app.Listen(3000) -} -``` -
- -### JSON Response - -๐Ÿ“– [JSON](https://docs.gofiber.io/context#json) - -
- -```go -type User struct { - Name string `json:"name"` - Age int `json:"age"` -} - -func main() { - app := fiber.New() - - app.Get("/user", func(c *fiber.Ctx) { - c.JSON(&User{"John", 20}) - // => {"name":"John", "age":20} - }) - - app.Get("/json", func(c *fiber.Ctx) { - c.JSON(fiber.Map{ - "success": true, - "message": "Hi John!", - }) - // => {"success":true, "message":"Hi John!"} - }) - - app.Listen(3000) -} -``` -
- - - -### WebSocket Upgrade - -๐Ÿ“– [Websocket](https://docs.gofiber.io/middleware#websocket) - -
- -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/websocket" -) - -func main() { - app := fiber.New() - - app.Get("/ws", websocket.New(func(c *websocket.Conn) { - for { - mt, msg, err := c.ReadMessage() - if err != nil { - log.Println("read:", err) - break - } - log.Printf("recv: %s", msg) - err = c.WriteMessage(mt, msg) - if err != nil { - log.Println("write:", err) - break - } - } - })) - - app.Listen(3000) - // ws://localhost:3000/ws -} -``` - -
- -### Recover middleware - -๐Ÿ“– [Recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) - -
- -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - app.Use(middleware.Recover()) - - app.Get("/", func(c *fiber.Ctx) { - panic("normally this would crash your app") - }) - - app.Listen(3000) -} -``` -
-
- -## ๐Ÿงฌ Fiber Middleware - -The Fiber middleware modules listed here are maintained by the [Fiber team](https://github.com/orgs/gofiber/people). - - -| Middleware | Description | Built-in middleware | -| :--- | :--- | :--- | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | - | -| [basicauth](https://github.com/gofiber/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | - | -| [compress](https://github.com/gofiber/fiber/blob/master/middleware/compress.md) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | `middleware.Compress()` | -| [cors](https://github.com/gofiber/cors) | Enable cross-origin resource sharing \(CORS\) with various options. | - | -| [csrf](https://github.com/gofiber/csrf) | Protect from CSRF exploits. | - | -| [filesystem](https://github.com/gofiber/fiber/blob/master/middleware/filesystem.md) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | - | -| [favicon](https://github.com/gofiber/fiber/blob/master/middleware/favicon.md) | Ignore favicon from logs or serve from memory if a file path is provided. | `middleware.Favicon()` | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | - | -| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | - | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | - | -| [limiter](https://github.com/gofiber/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | - | -| [logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) | HTTP request/response logger. | `middleware.Logger()` | -| [pprof](https://github.com/gofiber/pprof) | Special thanks to Matthew Lee \(@mthli\) | - | -| [recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) | Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized[ ErrorHandler](error-handling.md). | `middleware.Recover()` | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | - | -| [requestid](https://github.com/gofiber/fiber/blob/master/middleware/request_id.md) | Request ID middleware generates a unique id for a request. | `middleware.RequestID()` | -| [session](https://github.com/gofiber/session) | This session middleware is build on top of fasthttp/session by @savsgio MIT. Special thanks to @thomasvvugt for helping with this middleware. | - | -| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | - | -| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | - | - -## ๐ŸŒฑ Third Party Middlewares - -ู‡ุฐู‡ ู‚ุงุฆู…ุฉ middlewares ุงู„ุชูŠ ุชู… ุฅู†ุดุงุคู‡ุง ู…ู† ู‚ุจู„ ุงู„ู…ุฌุชู…ุน Fiber , ุงู„ุฑุฌุงุก ุฅู†ุดุงุกPR ุฅุฐุง ูƒู†ุช ุชุฑูŠุฏ ุฃู† ุชุฑู‰ ุฐู„ูƒ! -- [arsmn/fiber-casbin](https://github.com/arsmn/fiber-casbin) -- [arsmn/fiber-introspect](https://github.com/arsmn/fiber-introspect) -- [arsmn/fiber-swagger](https://github.com/arsmn/fiber-swagger) -- [arsmn/gqlgen](https://github.com/arsmn/gqlgen) -- [codemicro/fiber-cache](https://github.com/codemicro/fiber-cache) -- [itsursujit/fiber-boilerplate](https://github.com/itsursujit/fiber-boilerplate) -- [juandiii/go-jwk-security](https://github.com/juandiii/go-jwk-security) -- [kiyonlin/fiber_limiter](https://github.com/kiyonlin/fiber_limiter) -- [shareed2k/fiber_limiter](https://github.com/shareed2k/fiber_limiter) -- [shareed2k/fiber_tracing](https://github.com/shareed2k/fiber_tracing) -- [thomasvvugt/fiber-boilerplate](https://github.com/thomasvvugt/fiber-boilerplate) - -## ๐Ÿ’ฌ ูˆุณุงุฆู„ ุงู„ุฅุนู„ุงู… - -

- - -

- -- [Welcome to Fiber โ€” an Express.js styled web framework written in Go with โค๏ธ](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) โ€” _03 Feb 2020_ -- [Fiber released v1.7! ๐ŸŽ‰ What's new and is it still fast, flexible and friendly?](https://dev.to/koddr/fiber-v2-is-out-now-what-s-new-and-is-he-still-fast-flexible-and-friendly-3ipf) โ€” _21 Feb 2020_ -- [๐Ÿš€ Fiber v1.8. What's new, updated and re-thinked?](https://dev.to/koddr/fiber-v1-8-what-s-new-updated-and-re-thinked-339h) โ€” _03 Mar 2020_ -- [Is switching from Express to Fiber worth it? ๐Ÿค”](https://dev.to/koddr/are-sure-what-your-lovely-web-framework-running-so-fast-2jl1) โ€” _01 Apr 2020_ -- [Creating Fast APIs In Go Using Fiber](https://dev.to/jozsefsallai/creating-fast-apis-in-go-using-fiber-59m9) โ€” _07 Apr 2020_ -- [Building a Basic REST API in Go using Fiber](https://tutorialedge.net/golang/basic-rest-api-go-fiber/) - _23 Apr 2020_ -- [๐Ÿ“บ Building a REST API using GORM and Fiber](https://youtu.be/Iq2qT0fRhAA) - _25 Apr 2020_ -- [๐ŸŒŽ Create a travel list app with Go, Fiber, Angular, MongoDB and Google Cloud Secret Manager](https://blog.yongweilun.me/create-a-travel-list-app-with-go-fiber-angular-mongodb-and-google-cloud-secret-manager-ck9fgxy0p061pcss1xt1ubu8t) - _25 Apr 2020_ -- [Fiber v1.9.6 ๐Ÿ”ฅ How to improve performance by 817% and stay fast, flexible and friendly?](https://dev.to/koddr/fiber-v1-9-5-how-to-improve-performance-by-817-and-stay-fast-flexible-and-friendly-2dp6) - _12 May 2020_ -- [The road to web-based authentication with Fiber โšก](https://vugt.me/the-road-to-web-based-authentication-with-fiber/) - _20 May 2020_ -- [Building an Express-style API in Go with Fiber](https://blog.logrocket.com/express-style-api-go-fiber/) - _10 June 2020_ -- [ๅŸบไบŽgolang fiberๅ’Œangularๅผ€ๅ‘web](https://zhuanlan.zhihu.com/p/148925642) - _19 June 2020_ -- [ๅŸบไบŽๅปถ่ฟŸ่ฎก็ฎ—ไปค็‰Œๆกถ็š„gofiber้ข‘็Ž‡้™ๅˆถไธญ้—ดไปถๅฎž็Žฐ](https://zhuanlan.zhihu.com/p/149308936) - _20 June 2020_ -- [Construir una API en Golang con Fiber ๐Ÿ‡ช๐Ÿ‡ธ](https://enbonnet.me/article/53/construir-api-golang-con-fiber) - _28 June 2020_ -- [๐Ÿ“บWhy Go Fiber Is THE New Framework To Learn](https://www.youtube.com/watch?v=kvwsPeWDLM8) - _29 June 2020_ -- [่งฃๆžGofiber่ทฏ็”ฑ็ฎก็†](https://zhuanlan.zhihu.com/p/152494502) - _08 July 2020_ -- [๐Ÿ“บ Introduction to Fiber - An Express-inspired web framework](https://youtu.be/MfFi4Gt-tos) - _25 July 2020_ - -## ๐Ÿ‘ ู…ุณุงู‡ู…ุฉ - -ุฅุฐุง ูƒู†ุช ุชุฑูŠุฏ ุฃู† ุชู‚ูˆู„ **ุดูƒุฑุง ุฌุฒูŠู„** ูˆ/ุงูˆ ุฏุนู… ุงู„ุชู†ู…ูŠุฉ ุงู„ู†ุดุทุฉ ู„ู„ู€ `Fiber`: - -1. ุงุถู [GitHub ู†ุฌู…ุฉ](https://github.com/gofiber/fiber/stargazers) ู„ู„ู…ุดุฑูˆุน. -2. ุบุฑุฏ ุนู† ุงู„ู…ุดุฑูˆุน [ููŠ ุชูˆูŠุชุฑ ](https://twitter.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). -3. ุงูƒุชุจ ู…ุฑุงุฌุนุฉ ุฃูˆ ุจุฑู†ุงู…ุฌ ุชุนู„ูŠู…ูŠ ุนู† [Medium](https://medium.com/), [Dev.to](https://dev.to/) ุงูˆ ููŠ ู…ูˆู‚ุนูƒ ุงู„ุดุฎุตูŠ. -4. ุณุงุนุฏู†ุง ููŠ ุชุฑุฌู…ุฉ ู…ูˆู‚ุนู†ุง API ุงู„ุชูˆุซูŠู‚ ุนุจุฑ [Crowdin](https://crowdin.com/project/gofiber) [![Crowdin](https://badges.crowdin.net/gofiber/localized.svg)](https://crowdin.com/project/gofiber) -5. ุฏุนู… ุงู„ู…ุดุฑูˆุน ุจุงู„ุชุจุฑุน ุจู€ [ูƒูˆุจ ู…ู† ุงู„ู‚ู‡ูˆุฉ](https://buymeacoff.ee/fenny). - -## โ˜• ุงู„ุฏุงุนู…ูŠู† - -Fiber ู‡ูˆ ู…ุดุฑูˆุน ู…ูุชูˆุญ ุงู„ู…ุตุฏุฑ ูŠุนู…ู„ ุนู„ู‰ ุงู„ุชุจุฑุนุงุช ู„ุฏูุน ุงู„ููˆุงุชูŠุฑ ุŒ ุนู„ู‰ ุณุจูŠู„ ุงู„ู…ุซุงู„ ุงุณู… ุงู„ู†ุทุงู‚ ุงู„ุฎุงุต ุจู†ุง , gitbook, netlify and serverless ุงู„ุงุณุชุถุงูุฉ. ุฅุฐุง ูƒู†ุช ุชุฑูŠุฏ ุฏุนู… Fiber, ุชุณุชุทูŠุน โ˜• [**ุดุฑุงุก ูƒูˆุจ ู‚ู‡ูˆุฉ ู‡ู†ุง**](https://buymeacoff.ee/fenny). - -| | ุงู„ู…ุณุชุฎุฏู… | ุงู„ุชุจุฑุน | -| :---------------------------------------------------------- | :----------------------------------------------- | :-------- | -| ![](https://avatars.githubusercontent.com/u/204341?s=25 ) | [@destari](https://github.com/destari) | โ˜• x 10 | -| ![](https://avatars.githubusercontent.com/u/63164982?s=25 ) | [@dembygenesis](https://github.com/dembygenesis) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/56607882?s=25 ) | [@thomasvvugt](https://github.com/thomasvvugt) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/27820675?s=25 ) | [@hendratommy](https://github.com/hendratommy) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/1094221?s=25 ) | [@ekaputra07](https://github.com/ekaputra07) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/194590?s=25 ) | [@jorgefuertes](https://github.com/jorgefuertes) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/186637?s=25 ) | [@candidosales](https://github.com/candidosales) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/29659953?s=25 ) | [@l0nax](https://github.com/l0nax) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/59947262?s=25 ) | [@ankush](https://github.com/ankush) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/635852?s=25 ) | [@bihe](https://github.com/bihe) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/307334?s=25 ) | [@justdave](https://github.com/justdave) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/11155743?s=25 ) | [@koddr](https://github.com/koddr) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/29042462?s=25 ) | [@lapolinar](https://github.com/lapolinar) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/2978730?s=25 ) | [@diegowifi](https://github.com/diegowifi) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/44171355?s=25 ) | [@ssimk0](https://github.com/ssimk0) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/5638101?s=25 ) | [@raymayemir](https://github.com/raymayemir) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/619996?s=25 ) | [@melkorm](https://github.com/melkorm) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31022056?s=25 ) | [@marvinjwendt](https://github.com/thomasvvugt) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31921460?s=25 ) | [@toishy](https://github.com/toishy) | โ˜• x 1 | - -## โ€Žโ€๐Ÿ’ป ุงู„ู…ุณุงู‡ู…ูˆู† ููŠ ูƒุชุงุจุฉ ุงู„ูƒูˆุฏ - -Code Contributors - -## โญ๏ธ Stargazers - -Stargazers over time - -## โš ๏ธ ุฑุฎุตุฉ - -Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` ู‡ูˆ ุจุฑู†ุงู…ุฌ ู…ุฌุงู†ูŠ ูˆู…ูุชูˆุญ ุงู„ู…ุตุฏุฑ ู…ุฑุฎุต ุจู…ูˆุฌุจ [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). ุชู… ุฅู†ุดุงุก ุงู„ุดุนุงุฑ ุงู„ุฑุณู…ูŠ ู…ู† ู‚ุจู„ [Vic Shรณstak](https://github.com/koddr) ูˆูˆุฒุนุช ุชุญุช [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) ุฑุฎุตุฉ (CC BY-SA 4.0 International). - -**Third-party library licenses** -- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) -- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) -- [fasttemplate](https://github.com/valyala/fasttemplate/blob/master/LICENSE) -- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) diff --git a/.github/README_de.md b/.github/README_de.md deleted file mode 100644 index 587ad613..00000000 --- a/.github/README_de.md +++ /dev/null @@ -1,591 +0,0 @@ -

- - Fiber - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -

-

-Fiber ist ein von Expressjs inspiriertes Web-Framework, aufgebaut auf Fasthttp - die schnellste HTTP engine fรผr Go. Kreiert um Dinge zu vereinfachen, fรผr schnelle Entwicklung mit keinen Speicherzuweisungen und Performance im Hinterkopf. -

- -## โšก๏ธ Schnellstart - -```go -package main - -import "github.com/gofiber/fiber" - -func main() { - app := fiber.New() - - app.Get("/", func(c *fiber.Ctx) { - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -## ๐Ÿค– Benchmarks - -Diese Tests wurden von [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) und [Go Web](https://github.com/smallnest/go-web-framework-benchmark) ausgefรผhrt. Falls du alle Resultate sehen mรถchtest, besuche bitte unser [Wiki](https://docs.gofiber.io/benchmarks). - -

- - -

- -## โš™๏ธ Installation - -Als erstes, [downloade](https://golang.org/dl/) und installiere Go. `1.11` oder hรถher. - -Die Installation wird durch das [`go get`](https://golang.org/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) Kommando gestartet: - -```bash -go get -u github.com/gofiber/fiber -``` - -## ๐ŸŽฏ Eigenschaften - -- Robustes [Routing](https://docs.gofiber.io/routing) -- Bereitstellen von [statischen Dateien](https://docs.gofiber.io/application#static) -- Extreme [Performance](https://docs.gofiber.io/benchmarks) -- [Geringe Arbeitsspeicher](https://docs.gofiber.io/benchmarks) verwendung -- Express [API Endpunkte](https://docs.gofiber.io/context) -- [Middleware](https://docs.gofiber.io/middleware) & [Next](https://docs.gofiber.io/context#next) Support -- [Schnelle](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) serverseitige Programmierung -- [Template engines](https://github.com/gofiber/template) -- [WebSocket support](https://docs.gofiber.io/middleware#websocket) -- [Rate Limiter](https://docs.gofiber.io/middleware#limiter) -- Available in [15 languages](https://docs.gofiber.io/) -- Und vieles mehr - [erkunde Fiber](https://docs.gofiber.io/) - -## ๐Ÿ’ก Philosophie - -Neue gopher welche von [Node.js](https://nodejs.org/en/about/) zu [Go](https://golang.org/doc/) umsteigen, mรผssen eine Lernkurve durchlaufen, bevor sie ihre Webanwendungen oder Microservices erstellen kรถnnen. Fiber, als ein **Web-Framework**, wurde erschaffen mit der Idee von **Minimalismus** und folgt dem **UNIX Weg** damit neue Gophers mit einem herzlichen und vertrauenswรผrdigen Willkommen schnell in die Welt von Go eintreten kรถnnen. - -Fiber ist **inspiriert** von Expressjs, dem beliebtesten Web-Framework im Internet. Wir haben die **Leichtigkeit** von Express und die **Rohleistung** von Go kombiniert. Wenn du jemals eine Webanwendung mit Node.js implementiert hast (_mit Express.js oder รคhnlichem_), werden dir viele Methoden und Prinzipien **sehr vertraut** vorkommen. - -## ๐Ÿ‘€ Beispiele - -Nachfolgend sind einige der gรคngigen Beispiele aufgefรผhrt. Wenn du weitere Codebeispiele sehen mรถchten, besuche bitte unser ["Recipes Repository"](https://github.com/gofiber/recipes) oder besuche unsere [API Dokumentation](https://docs.gofiber.io). - -Listed below are some of the common examples. If you want to see more code examples , please visit our [Recipes repository](https://github.com/gofiber/recipes) or visit our hosted [API documentation](https://docs.gofiber.io). - -#### ๐Ÿ“– [**Basic Routing**](https://docs.gofiber.io/#basic-routing) - -```go -func main() { - app := fiber.New() - - // GET /john - app.Get("/:name", func(c *fiber.Ctx) { - msg := fmt.Sprintf("Hello, %s ๐Ÿ‘‹!", c.Params("name")) - c.Send(msg) // => Hello john ๐Ÿ‘‹! - }) - - // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ‘ด %s is %s years old", c.Params("name"), c.Params("age")) - c.Send(msg) // => ๐Ÿ‘ด john is 75 years old - }) - - // GET /dictionary.txt - app.Get("/:file.:ext", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ“ƒ %s.%s", c.Params("file"), c.Params("ext")) - c.Send(msg) // => ๐Ÿ“ƒ dictionary.txt - }) - - // GET /flights/LAX-SFO - app.Get("/flights/:from-:to", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ’ธ From: %s, To: %s", c.Params("from"), c.Params("to")) - c.Send(msg) // => ๐Ÿ’ธ From: LAX, To: SFO - }) - - // GET /api/register - app.Get("/api/*", func(c *fiber.Ctx) { - msg := fmt.Sprintf("โœ‹ %s", c.Params("*")) - c.Send(msg) // => โœ‹ /api/register - }) - - app.Listen(3000) -} -``` - -#### ๐Ÿ“– [**Serving Static Files**](https://docs.gofiber.io/application#static) - -```go -func main() { - app := fiber.New() - - app.Static("/", "./public") - // => http://localhost:3000/js/script.js - // => http://localhost:3000/css/style.css - - app.Static("/prefix", "./public") - // => http://localhost:3000/prefix/js/script.js - // => http://localhost:3000/prefix/css/style.css - - app.Static("*", "./public/index.html") - // => http://localhost:3000/any/path/shows/index/html - - app.Listen(3000) -} -``` - -#### ๐Ÿ“– [**Middleware & Next**](https://docs.gofiber.io/context#next) - -```go -func main() { - app := fiber.New() - - // Match any route - app.Use(func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‡ First handler") - c.Next() - }) - - // Match all routes starting with /api - app.Use("/api", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅˆ Second handler") - c.Next() - }) - - // GET /api/register - app.Get("/api/list", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‰ Last handler") - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -
- ๐Ÿ“š Show more code examples - -### Views engines - -๐Ÿ“– [Settings](https://docs.gofiber.io/application#settings) -๐Ÿ“– [Engines](https://github.com/gofiber/template) -๐Ÿ“– [Render](https://docs.gofiber.io/context#render) - -Fiber defaults to the [html/template](https://golang.org/pkg/html/template/) when no view engine is set. - -If you want to execute partials or use a different engine like [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) or [pug](https://github.com/Joker/jade) etc.. - -Checkout our [Template](https://github.com/gofiber/template) package that support multiple view engines. - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/template/pug" -) - -func main() { - // You can setup Views engine before initiation app: - app := fiber.New(&fiber.Settings{ - Views: pug.New("./views", ".pug"), - }) - - // OR after initiation app at any convenient location: - app.Settings.Views = pug.New("./views", ".pug"), - - // And now, you can call template `./views/home.pug` like this: - app.Get("/", func(c *fiber.Ctx) { - c.Render("home", fiber.Map{ - "title": "Homepage", - "year": 1999, - }) - }) - - // ... -} -``` - -### Grouping routes into chains - -๐Ÿ“– [Group](https://docs.gofiber.io/application#group) - -```go -func main() { - app := fiber.New() - - // Root API route - api := app.Group("/api", cors()) // /api - - // API v1 routes - v1 := api.Group("/v1", mysql()) // /api/v1 - v1.Get("/list", handler) // /api/v1/list - v1.Get("/user", handler) // /api/v1/user - - // API v2 routes - v2 := api.Group("/v2", mongodb()) // /api/v2 - v2.Get("/list", handler) // /api/v2/list - v2.Get("/user", handler) // /api/v2/user - - // ... -} -``` - -### Middleware logger - -๐Ÿ“– [Logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - // Default - app.Use(middleware.Logger()) - - // Custom logging format - app.Use(middleware.Logger("${method} - ${path}")) - - // Custom Config - app.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ - Next: func(ctx *fiber.Ctx) bool { - return ctx.Path() != "/private" - }, - Format: "${method} - ${path}", - Output: io.Writer, - })) - - app.Listen(3000) -} -`````` - -### Cross-Origin Resource Sharing (CORS) - -๐Ÿ“– [CORS](https://docs.gofiber.io/middleware#cors) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/cors" -) - -func main() { - app := fiber.New() - - // CORS with default config - app.Use(cors.New()) - - app.Listen(3000) -} -``` - -Check CORS by passing any domain in `Origin` header: - -```bash -curl -H "Origin: http://example.com" --verbose http://localhost:3000 -``` - -### Custom 404 response - -๐Ÿ“– [HTTP Methods](https://docs.gofiber.io/application#http-methods) - -```go -func main() { - app := fiber.New() - - app.Static("./public") - - app.Get("/demo", func(c *fiber.Ctx) { - c.Send("This is a demo!") - }) - - app.Post("/register", func(c *fiber.Ctx) { - c.Send("Welcome!") - }) - - // Last middleware to match anything - app.Use(func(c *fiber.Ctx) { - c.SendStatus(404) - // => 404 "Not Found" - }) - - app.Listen(3000) -} -``` - -### JSON Response - -๐Ÿ“– [JSON](https://docs.gofiber.io/context#json) - -```go -type User struct { - Name string `json:"name"` - Age int `json:"age"` -} - -func main() { - app := fiber.New() - - app.Get("/user", func(c *fiber.Ctx) { - c.JSON(&User{"John", 20}) - // => {"name":"John", "age":20} - }) - - app.Get("/json", func(c *fiber.Ctx) { - c.JSON(fiber.Map{ - "success": true, - "message": "Hi John!", - }) - // => {"success":true, "message":"Hi John!"} - }) - - app.Listen(3000) -} -``` - -### WebSocket Upgrade - -๐Ÿ“– [Websocket](https://docs.gofiber.io/middleware#websocket) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/websocket" -) - -func main() { - app := fiber.New() - - app.Get("/ws", websocket.New(func(c *websocket.Conn) { - for { - mt, msg, err := c.ReadMessage() - if err != nil { - log.Println("read:", err) - break - } - log.Printf("recv: %s", msg) - err = c.WriteMessage(mt, msg) - if err != nil { - log.Println("write:", err) - break - } - } - })) - - app.Listen(3000) - // ws://localhost:3000/ws -} -``` - -### Recover middleware - -๐Ÿ“– [Recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - app.Use(middleware.Recover()) - - app.Get("/", func(c *fiber.Ctx) { - panic("normally this would crash your app") - }) - - app.Listen(3000) -} -``` -
- -## ๐Ÿงฌ Fiber Middleware - -The Fiber middleware modules listed here are maintained by the [Fiber team](https://github.com/orgs/gofiber/people). - -| Middleware | Description | Built-in middleware | -| :--- | :--- | :--- | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | - | -| [basicauth](https://github.com/gofiber/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | - | -| [compress](https://github.com/gofiber/fiber/blob/master/middleware/compress.md) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | `middleware.Compress()` | -| [cors](https://github.com/gofiber/cors) | Enable cross-origin resource sharing \(CORS\) with various options. | - | -| [csrf](https://github.com/gofiber/csrf) | Protect from CSRF exploits. | - | -| [filesystem](https://github.com/gofiber/fiber/blob/master/middleware/filesystem.md) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | - | -| [favicon](https://github.com/gofiber/fiber/blob/master/middleware/favicon.md) | Ignore favicon from logs or serve from memory if a file path is provided. | `middleware.Favicon()` | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | - | -| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | - | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | - | -| [limiter](https://github.com/gofiber/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | - | -| [logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) | HTTP request/response logger. | `middleware.Logger()` | -| [pprof](https://github.com/gofiber/pprof) | Special thanks to Matthew Lee \(@mthli\) | - | -| [recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) | Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized[ ErrorHandler](error-handling.md). | `middleware.Recover()` | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | - | -| [requestid](https://github.com/gofiber/fiber/blob/master/middleware/request_id.md) | Request ID middleware generates a unique id for a request. | `middleware.RequestID()` | -| [session](https://github.com/gofiber/session) | This session middleware is build on top of fasthttp/session by @savsgio MIT. Special thanks to @thomasvvugt for helping with this middleware. | - | -| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | - | -| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | - | - -## ๐ŸŒฑ Third Party Middlewares - -This is a list of middlewares that are created by the Fiber community, please create a PR if you want to see yours! -- [arsmn/fiber-casbin](https://github.com/arsmn/fiber-casbin) -- [arsmn/fiber-introspect](https://github.com/arsmn/fiber-introspect) -- [arsmn/fiber-swagger](https://github.com/arsmn/fiber-swagger) -- [arsmn/gqlgen](https://github.com/arsmn/gqlgen) -- [codemicro/fiber-cache](https://github.com/codemicro/fiber-cache) -- [itsursujit/fiber-boilerplate](https://github.com/itsursujit/fiber-boilerplate) -- [juandiii/go-jwk-security](https://github.com/juandiii/go-jwk-security) -- [kiyonlin/fiber_limiter](https://github.com/kiyonlin/fiber_limiter) -- [shareed2k/fiber_limiter](https://github.com/shareed2k/fiber_limiter) -- [shareed2k/fiber_tracing](https://github.com/shareed2k/fiber_tracing) -- [thomasvvugt/fiber-boilerplate](https://github.com/thomasvvugt/fiber-boilerplate) - -## ๐Ÿ’ฌ Medien - -

- - -

- -- [Welcome to Fiber โ€” an Express.js styled web framework written in Go with โค๏ธ](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) โ€” _03 Feb 2020_ -- [Fiber released v1.7! ๐ŸŽ‰ What's new and is it still fast, flexible and friendly?](https://dev.to/koddr/fiber-v2-is-out-now-what-s-new-and-is-he-still-fast-flexible-and-friendly-3ipf) โ€” _21 Feb 2020_ -- [๐Ÿš€ Fiber v1.8. What's new, updated and re-thinked?](https://dev.to/koddr/fiber-v1-8-what-s-new-updated-and-re-thinked-339h) โ€” _03 Mar 2020_ -- [Is switching from Express to Fiber worth it? ๐Ÿค”](https://dev.to/koddr/are-sure-what-your-lovely-web-framework-running-so-fast-2jl1) โ€” _01 Apr 2020_ -- [Creating Fast APIs In Go Using Fiber](https://dev.to/jozsefsallai/creating-fast-apis-in-go-using-fiber-59m9) โ€” _07 Apr 2020_ -- [Building a Basic REST API in Go using Fiber](https://tutorialedge.net/golang/basic-rest-api-go-fiber/) - _23 Apr 2020_ -- [๐Ÿ“บ Building a REST API using GORM and Fiber](https://youtu.be/Iq2qT0fRhAA) - _25 Apr 2020_ -- [๐ŸŒŽ Create a travel list app with Go, Fiber, Angular, MongoDB and Google Cloud Secret Manager](https://blog.yongweilun.me/create-a-travel-list-app-with-go-fiber-angular-mongodb-and-google-cloud-secret-manager-ck9fgxy0p061pcss1xt1ubu8t) - _25 Apr 2020_ -- [Fiber v1.9.6 ๐Ÿ”ฅ How to improve performance by 817% and stay fast, flexible and friendly?](https://dev.to/koddr/fiber-v1-9-5-how-to-improve-performance-by-817-and-stay-fast-flexible-and-friendly-2dp6) - _12 May 2020_ -- [The road to web-based authentication with Fiber โšก](https://vugt.me/the-road-to-web-based-authentication-with-fiber/) - _20 May 2020_ -- [Building an Express-style API in Go with Fiber](https://blog.logrocket.com/express-style-api-go-fiber/) - _10 June 2020_ -- [ๅŸบไบŽgolang fiberๅ’Œangularๅผ€ๅ‘web](https://zhuanlan.zhihu.com/p/148925642) - _19 June 2020_ -- [ๅŸบไบŽๅปถ่ฟŸ่ฎก็ฎ—ไปค็‰Œๆกถ็š„gofiber้ข‘็Ž‡้™ๅˆถไธญ้—ดไปถๅฎž็Žฐ](https://zhuanlan.zhihu.com/p/149308936) - _20 June 2020_ -- [Construir una API en Golang con Fiber ๐Ÿ‡ช๐Ÿ‡ธ](https://enbonnet.me/article/53/construir-api-golang-con-fiber) - _28 June 2020_ -- [๐Ÿ“บWhy Go Fiber Is THE New Framework To Learn](https://www.youtube.com/watch?v=kvwsPeWDLM8) - _29 June 2020_ -- [่งฃๆžGofiber่ทฏ็”ฑ็ฎก็†](https://zhuanlan.zhihu.com/p/152494502) - _08 July 2020_ -- [๐Ÿ“บ Introduction to Fiber - An Express-inspired web framework](https://youtu.be/MfFi4Gt-tos) - _25 July 2020_ - -## ๐Ÿ‘ Mitwirken - -Falls du **danke** sagen mรถchtest und/oder aktiv die Entwicklung von `fiber` fรถrdern mรถchtest: - -1. Fรผge dem Projekt einen [GitHub Stern](https://github.com/gofiber/fiber/stargazers) hinzu. -2. Twittere รผber das Projekt [auf deinem Twitter](https://twitter.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). -3. Schreibe eine Rezension auf [Medium](https://medium.com/), [Dev.to](https://dev.to/) oder einem persรถnlichem Blog. -4. Help us to translate our API Documentation via [Crowdin](https://crowdin.com/project/gofiber) [![Crowdin](https://badges.crowdin.net/gofiber/localized.svg)](https://crowdin.com/project/gofiber) -5. Support the project by donating a [cup of coffee](https://buymeacoff.ee/fenny). - -## โ˜• Supporters - -Fiber is an open source project that runs on donations to pay the bills e.g. our domain name, gitbook, netlify and serverless hosting. If you want to support Fiber, you can โ˜• [**buy a coffee here**](https://buymeacoff.ee/fenny). - -| | User | Donation | -| :---------------------------------------------------------- | :----------------------------------------------- | :-------- | -| ![](https://avatars.githubusercontent.com/u/204341?s=25 ) | [@destari](https://github.com/destari) | โ˜• x 10 | -| ![](https://avatars.githubusercontent.com/u/63164982?s=25 ) | [@dembygenesis](https://github.com/dembygenesis) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/56607882?s=25 ) | [@thomasvvugt](https://github.com/thomasvvugt) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/27820675?s=25 ) | [@hendratommy](https://github.com/hendratommy) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/1094221?s=25 ) | [@ekaputra07](https://github.com/ekaputra07) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/194590?s=25 ) | [@jorgefuertes](https://github.com/jorgefuertes) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/186637?s=25 ) | [@candidosales](https://github.com/candidosales) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/29659953?s=25 ) | [@l0nax](https://github.com/l0nax) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/59947262?s=25 ) | [@ankush](https://github.com/ankush) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/635852?s=25 ) | [@bihe](https://github.com/bihe) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/307334?s=25 ) | [@justdave](https://github.com/justdave) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/11155743?s=25 ) | [@koddr](https://github.com/koddr) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/29042462?s=25 ) | [@lapolinar](https://github.com/lapolinar) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/2978730?s=25 ) | [@diegowifi](https://github.com/diegowifi) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/44171355?s=25 ) | [@ssimk0](https://github.com/ssimk0) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/5638101?s=25 ) | [@raymayemir](https://github.com/raymayemir) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/619996?s=25 ) | [@melkorm](https://github.com/melkorm) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31022056?s=25 ) | [@marvinjwendt](https://github.com/thomasvvugt) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31921460?s=25 ) | [@toishy](https://github.com/toishy) | โ˜• x 1 | - -## โ€Žโ€๐Ÿ’ป Code Contributors - -Code Contributors - -## โญ๏ธ Stargazers - -Stargazers over time - -## โš ๏ธ License - -Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` is free and open-source software licensed under the [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Official logo was created by [Vic Shรณstak](https://github.com/koddr) and distributed under [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) license (CC BY-SA 4.0 International). - -**Third-party MIT licenses** -- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) -- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) -- [fasttemplate](https://github.com/valyala/fasttemplate/blob/master/LICENSE) -- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) diff --git a/.github/README_es.md b/.github/README_es.md deleted file mode 100644 index 0f01edfe..00000000 --- a/.github/README_es.md +++ /dev/null @@ -1,591 +0,0 @@ -

- - Fiber - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -

-

-Fiber es un framework web inspirado en Express construido sobre Fasthttp, el motor HTTP mรกs rรกpido para Go. Diseรฑado para facilitar las cosas para un desarrollo rรกpido con cero asignaciรณn de memoria y rendimiento en mente. -

- -## โšก๏ธ Inicio rรกpido - -```go -package main - -import "github.com/gofiber/fiber" - -func main() { - app := fiber.New() - - app.Get("/", func(c *fiber.Ctx) { - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -## ๐Ÿค– Puntos de referencia - -Estas pruebas son realizadas por [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) y [Go Web](https://github.com/smallnest/go-web-framework-benchmark) . Si desea ver todos los resultados, visite nuestro [Wiki](https://docs.gofiber.io/benchmarks) . - -

- - -

- -## โš™๏ธ Instalaciรณn - -En primer lugar, [descargue](https://golang.org/dl/) e instale Go. Se requiere `1.11` o superior. - -La instalaciรณn se realiza con el comando [`go get`](https://golang.org/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) : - -```bash -go get github.com/gofiber/fiber/... -``` - -## ๐ŸŽฏ Caracterรญsticas - -- [Enrutamiento](https://docs.gofiber.io/routing) robusto -- Servir [archivos estรกticos](https://docs.gofiber.io/application#static) -- [Rendimiento](https://docs.gofiber.io/benchmarks) extremo -- [Poca](https://docs.gofiber.io/benchmarks) huella de [memoria](https://docs.gofiber.io/benchmarks) -- [Puntos finales de API](https://docs.gofiber.io/context) Express -- Middleware y [prรณximo](https://docs.gofiber.io/context#next) soporte -- Programaciรณn [rรกpida](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) del lado del servidor -- [Template engines](https://github.com/gofiber/template) -- [WebSocket support](https://docs.gofiber.io/middleware#websocket) -- [Rate Limiter](https://docs.gofiber.io/middleware#limiter) -- Disponible en [15 idiomas](https://docs.gofiber.io/) -- Y mucho mรกs, [explora Fiber](https://docs.gofiber.io/) - -## ๐Ÿ’ก Filosofรญa - -Los nuevos gophers que hacen el cambio de [Node.js](https://nodejs.org/en/about/) a [Go](https://golang.org/doc/) estรกn lidiando con una curva de aprendizaje antes de que puedan comenzar a construir sus aplicaciones web o microservicios. Fiber, como un **marco web** , fue creado con la idea del **minimalismo** y sigue el **camino de UNIX** , para que los nuevos gophers puedan ingresar rรกpidamente al mundo de Go con una cรกlida y confiable bienvenida. - -Fiber estรก **inspirado** en Expressjs, el framework web mรกs popular en Internet. Combinamos la **facilidad** de Express y **el rendimiento bruto** de Go. Si alguna vez ha implementado una aplicaciรณn web en Node.js ( *utilizando Express.js o similar* ), muchos mรฉtodos y principios le parecerรกn **muy comunes** . - -## ๐Ÿ‘€ Ejemplos - -A continuaciรณn se enumeran algunos de los ejemplos comunes. Si desea ver mรกs ejemplos de cรณdigo, visite nuestro [repositorio de Recetas](https://github.com/gofiber/recipes) o nuestra [documentaciรณn de API](https://docs.gofiber.io) . - -Listed below are some of the common examples. If you want to see more code examples , please visit our [Recipes repository](https://github.com/gofiber/recipes) or visit our hosted [API documentation](https://docs.gofiber.io). - -#### ๐Ÿ“– [**Basic Routing**](https://docs.gofiber.io/#basic-routing) - -```go -func main() { - app := fiber.New() - - // GET /john - app.Get("/:name", func(c *fiber.Ctx) { - msg := fmt.Sprintf("Hello, %s ๐Ÿ‘‹!", c.Params("name")) - c.Send(msg) // => Hello john ๐Ÿ‘‹! - }) - - // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ‘ด %s is %s years old", c.Params("name"), c.Params("age")) - c.Send(msg) // => ๐Ÿ‘ด john is 75 years old - }) - - // GET /dictionary.txt - app.Get("/:file.:ext", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ“ƒ %s.%s", c.Params("file"), c.Params("ext")) - c.Send(msg) // => ๐Ÿ“ƒ dictionary.txt - }) - - // GET /flights/LAX-SFO - app.Get("/flights/:from-:to", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ’ธ From: %s, To: %s", c.Params("from"), c.Params("to")) - c.Send(msg) // => ๐Ÿ’ธ From: LAX, To: SFO - }) - - // GET /api/register - app.Get("/api/*", func(c *fiber.Ctx) { - msg := fmt.Sprintf("โœ‹ %s", c.Params("*")) - c.Send(msg) // => โœ‹ /api/register - }) - - app.Listen(3000) -} -``` - -#### ๐Ÿ“– [**Serving Static Files**](https://docs.gofiber.io/application#static) - -```go -func main() { - app := fiber.New() - - app.Static("/", "./public") - // => http://localhost:3000/js/script.js - // => http://localhost:3000/css/style.css - - app.Static("/prefix", "./public") - // => http://localhost:3000/prefix/js/script.js - // => http://localhost:3000/prefix/css/style.css - - app.Static("*", "./public/index.html") - // => http://localhost:3000/any/path/shows/index/html - - app.Listen(3000) -} -``` - -#### ๐Ÿ“– [**Middleware & Next**](https://docs.gofiber.io/context#next) - -```go -func main() { - app := fiber.New() - - // Match any route - app.Use(func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‡ First handler") - c.Next() - }) - - // Match all routes starting with /api - app.Use("/api", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅˆ Second handler") - c.Next() - }) - - // GET /api/register - app.Get("/api/list", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‰ Last handler") - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -
- ๐Ÿ“š Mostrar mรกs ejemplos de cรณdigo - -### Views engines - -๐Ÿ“– [Settings](https://docs.gofiber.io/application#settings) -๐Ÿ“– [Engines](https://github.com/gofiber/template) -๐Ÿ“– [Render](https://docs.gofiber.io/context#render) - -Fiber defaults to the [html/template](https://golang.org/pkg/html/template/) when no view engine is set. - -If you want to execute partials or use a different engine like [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) or [pug](https://github.com/Joker/jade) etc.. - -Checkout our [Template](https://github.com/gofiber/template) package that support multiple view engines. - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/template/pug" -) - -func main() { - // You can setup Views engine before initiation app: - app := fiber.New(&fiber.Settings{ - Views: pug.New("./views", ".pug"), - }) - - // OR after initiation app at any convenient location: - app.Settings.Views = pug.New("./views", ".pug"), - - // And now, you can call template `./views/home.pug` like this: - app.Get("/", func(c *fiber.Ctx) { - c.Render("home", fiber.Map{ - "title": "Homepage", - "year": 1999, - }) - }) - - // ... -} -``` - -### Agrupando rutas en cadenas - -๐Ÿ“– [Group](https://docs.gofiber.io/application#group) - -```go -func main() { - app := fiber.New() - - // Root API route - api := app.Group("/api", cors()) // /api - - // API v1 routes - v1 := api.Group("/v1", mysql()) // /api/v1 - v1.Get("/list", handler) // /api/v1/list - v1.Get("/user", handler) // /api/v1/user - - // API v2 routes - v2 := api.Group("/v2", mongodb()) // /api/v2 - v2.Get("/list", handler) // /api/v2/list - v2.Get("/user", handler) // /api/v2/user - - // ... -} -``` - -### Middleware logger - -๐Ÿ“– [Logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - // Default - app.Use(middleware.Logger()) - - // Custom logging format - app.Use(middleware.Logger("${method} - ${path}")) - - // Custom Config - app.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ - Next: func(ctx *fiber.Ctx) bool { - return ctx.Path() != "/private" - }, - Format: "${method} - ${path}", - Output: io.Writer, - })) - - app.Listen(3000) -} -``` - -### Cross-Origin Resource Sharing (CORS) - -๐Ÿ“– [CORS](https://docs.gofiber.io/middleware#cors) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/cors" -) - -func main() { - app := fiber.New() - - // CORS with default config - app.Use(cors.New()) - - app.Listen(3000) -} -``` - -Check CORS by passing any domain in `Origin` header: - -```bash -curl -H "Origin: http://example.com" --verbose http://localhost:3000 -``` - -### Respuesta 404 personalizada - -๐Ÿ“– [HTTP Methods](https://docs.gofiber.io/application#http-methods) - -```go -func main() { - app := fiber.New() - - app.Static("./public") - - app.Get("/demo", func(c *fiber.Ctx) { - c.Send("This is a demo!") - }) - - app.Post("/register", func(c *fiber.Ctx) { - c.Send("Welcome!") - }) - - // Last middleware to match anything - app.Use(func(c *fiber.Ctx) { - c.SendStatus(404) - // => 404 "Not Found" - }) - - app.Listen(3000) -} -``` - -### Respuesta JSON - -๐Ÿ“– [JSON](https://docs.gofiber.io/context#json) - -```go -type User struct { - Name string `json:"name"` - Age int `json:"age"` -} - -func main() { - app := fiber.New() - - app.Get("/user", func(c *fiber.Ctx) { - c.JSON(&User{"John", 20}) - // => {"name":"John", "age":20} - }) - - app.Get("/json", func(c *fiber.Ctx) { - c.JSON(fiber.Map{ - "success": true, - "message": "Hi John!", - }) - // => {"success":true, "message":"Hi John!"} - }) - - app.Listen(3000) -} -``` - -### WebSocket Upgrade - -๐Ÿ“– [Websocket](https://docs.gofiber.io/middleware#websocket) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/websocket" -) - -func main() { - app := fiber.New() - - app.Get("/ws", websocket.New(func(c *websocket.Conn) { - for { - mt, msg, err := c.ReadMessage() - if err != nil { - log.Println("read:", err) - break - } - log.Printf("recv: %s", msg) - err = c.WriteMessage(mt, msg) - if err != nil { - log.Println("write:", err) - break - } - } - })) - - app.Listen(3000) - // ws://localhost:3000/ws -} -``` - -### Recover middleware - -๐Ÿ“– [Recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - app.Use(middleware.Recover()) - - app.Get("/", func(c *fiber.Ctx) { - panic("normally this would crash your app") - }) - - app.Listen(3000) -} -``` -
- -## ๐Ÿงฌ Fiber Middleware - -The Fiber middleware modules listed here are maintained by the [Fiber team](https://github.com/orgs/gofiber/people). - -| Middleware | Description | Built-in middleware | -| :--- | :--- | :--- | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | - | -| [basicauth](https://github.com/gofiber/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | - | -| [compress](https://github.com/gofiber/fiber/blob/master/middleware/compress.md) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | `middleware.Compress()` | -| [cors](https://github.com/gofiber/cors) | Enable cross-origin resource sharing \(CORS\) with various options. | - | -| [csrf](https://github.com/gofiber/csrf) | Protect from CSRF exploits. | - | -| [filesystem](https://github.com/gofiber/fiber/blob/master/middleware/filesystem.md) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | - | -| [favicon](https://github.com/gofiber/fiber/blob/master/middleware/favicon.md) | Ignore favicon from logs or serve from memory if a file path is provided. | `middleware.Favicon()` | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | - | -| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | - | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | - | -| [limiter](https://github.com/gofiber/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | - | -| [logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) | HTTP request/response logger. | `middleware.Logger()` | -| [pprof](https://github.com/gofiber/pprof) | Special thanks to Matthew Lee \(@mthli\) | - | -| [recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) | Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized[ ErrorHandler](error-handling.md). | `middleware.Recover()` | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | - | -| [requestid](https://github.com/gofiber/fiber/blob/master/middleware/request_id.md) | Request ID middleware generates a unique id for a request. | `middleware.RequestID()` | -| [session](https://github.com/gofiber/session) | This session middleware is build on top of fasthttp/session by @savsgio MIT. Special thanks to @thomasvvugt for helping with this middleware. | - | -| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | - | -| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | - | - -## ๐ŸŒฑ Third Party Middlewares - -This is a list of middlewares that are created by the Fiber community, please create a PR if you want to see yours! -- [arsmn/fiber-casbin](https://github.com/arsmn/fiber-casbin) -- [arsmn/fiber-introspect](https://github.com/arsmn/fiber-introspect) -- [arsmn/fiber-swagger](https://github.com/arsmn/fiber-swagger) -- [arsmn/gqlgen](https://github.com/arsmn/gqlgen) -- [codemicro/fiber-cache](https://github.com/codemicro/fiber-cache) -- [itsursujit/fiber-boilerplate](https://github.com/itsursujit/fiber-boilerplate) -- [juandiii/go-jwk-security](https://github.com/juandiii/go-jwk-security) -- [kiyonlin/fiber_limiter](https://github.com/kiyonlin/fiber_limiter) -- [shareed2k/fiber_limiter](https://github.com/shareed2k/fiber_limiter) -- [shareed2k/fiber_tracing](https://github.com/shareed2k/fiber_tracing) -- [thomasvvugt/fiber-boilerplate](https://github.com/thomasvvugt/fiber-boilerplate) - -## ๐Ÿ’ฌ Medios - -

- - -

- -- [Welcome to Fiber โ€” an Express.js styled web framework written in Go with โค๏ธ](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) โ€” _03 Feb 2020_ -- [Fiber released v1.7! ๐ŸŽ‰ What's new and is it still fast, flexible and friendly?](https://dev.to/koddr/fiber-v2-is-out-now-what-s-new-and-is-he-still-fast-flexible-and-friendly-3ipf) โ€” _21 Feb 2020_ -- [๐Ÿš€ Fiber v1.8. What's new, updated and re-thinked?](https://dev.to/koddr/fiber-v1-8-what-s-new-updated-and-re-thinked-339h) โ€” _03 Mar 2020_ -- [Is switching from Express to Fiber worth it? ๐Ÿค”](https://dev.to/koddr/are-sure-what-your-lovely-web-framework-running-so-fast-2jl1) โ€” _01 Apr 2020_ -- [Creating Fast APIs In Go Using Fiber](https://dev.to/jozsefsallai/creating-fast-apis-in-go-using-fiber-59m9) โ€” _07 Apr 2020_ -- [Building a Basic REST API in Go using Fiber](https://tutorialedge.net/golang/basic-rest-api-go-fiber/) - _23 Apr 2020_ -- [๐Ÿ“บ Building a REST API using GORM and Fiber](https://youtu.be/Iq2qT0fRhAA) - _25 Apr 2020_ -- [๐ŸŒŽ Create a travel list app with Go, Fiber, Angular, MongoDB and Google Cloud Secret Manager](https://blog.yongweilun.me/create-a-travel-list-app-with-go-fiber-angular-mongodb-and-google-cloud-secret-manager-ck9fgxy0p061pcss1xt1ubu8t) - _25 Apr 2020_ -- [Fiber v1.9.6 ๐Ÿ”ฅ How to improve performance by 817% and stay fast, flexible and friendly?](https://dev.to/koddr/fiber-v1-9-5-how-to-improve-performance-by-817-and-stay-fast-flexible-and-friendly-2dp6) - _12 May 2020_ -- [The road to web-based authentication with Fiber โšก](https://vugt.me/the-road-to-web-based-authentication-with-fiber/) - _20 May 2020_ -- [Building an Express-style API in Go with Fiber](https://blog.logrocket.com/express-style-api-go-fiber/) - _10 June 2020_ -- [ๅŸบไบŽgolang fiberๅ’Œangularๅผ€ๅ‘web](https://zhuanlan.zhihu.com/p/148925642) - _19 June 2020_ -- [ๅŸบไบŽๅปถ่ฟŸ่ฎก็ฎ—ไปค็‰Œๆกถ็š„gofiber้ข‘็Ž‡้™ๅˆถไธญ้—ดไปถๅฎž็Žฐ](https://zhuanlan.zhihu.com/p/149308936) - _20 June 2020_ -- [Construir una API en Golang con Fiber ๐Ÿ‡ช๐Ÿ‡ธ](https://enbonnet.me/article/53/construir-api-golang-con-fiber) - _28 June 2020_ -- [๐Ÿ“บWhy Go Fiber Is THE New Framework To Learn](https://www.youtube.com/watch?v=kvwsPeWDLM8) - _29 June 2020_ -- [่งฃๆžGofiber่ทฏ็”ฑ็ฎก็†](https://zhuanlan.zhihu.com/p/152494502) - _08 July 2020_ -- [๐Ÿ“บ Introduction to Fiber - An Express-inspired web framework](https://youtu.be/MfFi4Gt-tos) - _25 July 2020_ - -## ๐Ÿ‘ Contribuir - -Si quiere **agradecer** y/o apoyar el desarrollo activo de `Fiber`: - -1. Agrega una [estrella de GitHub](https://github.com/gofiber/fiber/stargazers) al proyecto. -2. Tuitea sobre el proyecto [en tu Twitter](https://twitter.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). -3. Escribe una reseรฑa o tutorial en [Medium](https://medium.com/) , [Dev.to](https://dev.to/) o blog personal. -4. Ayรบdanos a traducir la documentaciรณn de nuestra API a travรฉs de [Crowdin](https://crowdin.com/project/gofiber) [![Crowdin](https://badges.crowdin.net/gofiber/localized.svg)](https://crowdin.com/project/gofiber) -5. Apoya el proyecto donando una [tasa de cafรฉ](https://buymeacoff.ee/fenny). - -## โ˜• Personas que han mostrado su apoyo - -Fiber es un proyecto open source que se mantiene a travรฉs de donaciones para pagar las cuentas e.g. nuestro nombre de dominio, gitbook, netlify y hosting serverless. Si quieres apoyar a Fiber, puedes โ˜• [**comprar un cafรฉ**](https://buymeacoff.ee/fenny). - -| | User | Donation | -| :---------------------------------------------------------- | :----------------------------------------------- | :-------- | -| ![](https://avatars.githubusercontent.com/u/204341?s=25 ) | [@destari](https://github.com/destari) | โ˜• x 10 | -| ![](https://avatars.githubusercontent.com/u/63164982?s=25 ) | [@dembygenesis](https://github.com/dembygenesis) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/56607882?s=25 ) | [@thomasvvugt](https://github.com/thomasvvugt) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/27820675?s=25 ) | [@hendratommy](https://github.com/hendratommy) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/1094221?s=25 ) | [@ekaputra07](https://github.com/ekaputra07) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/194590?s=25 ) | [@jorgefuertes](https://github.com/jorgefuertes) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/186637?s=25 ) | [@candidosales](https://github.com/candidosales) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/29659953?s=25 ) | [@l0nax](https://github.com/l0nax) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/59947262?s=25 ) | [@ankush](https://github.com/ankush) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/635852?s=25 ) | [@bihe](https://github.com/bihe) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/307334?s=25 ) | [@justdave](https://github.com/justdave) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/11155743?s=25 ) | [@koddr](https://github.com/koddr) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/29042462?s=25 ) | [@lapolinar](https://github.com/lapolinar) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/2978730?s=25 ) | [@diegowifi](https://github.com/diegowifi) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/44171355?s=25 ) | [@ssimk0](https://github.com/ssimk0) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/5638101?s=25 ) | [@raymayemir](https://github.com/raymayemir) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/619996?s=25 ) | [@melkorm](https://github.com/melkorm) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31022056?s=25 ) | [@marvinjwendt](https://github.com/thomasvvugt) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31921460?s=25 ) | [@toishy](https://github.com/toishy) | โ˜• x 1 | - -## โ€Žโ€๐Ÿ’ป Contribuyentes de cรณdigo - -Code Contributors - -## โญ๏ธ Stargazers - -Stargazers over time - -## โš ๏ธ Licencia - -Copyright (c) 2019-presente [Fenny](https://github.com/fenny) y [contribuyentes](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` es software libre y de cรณdigo abierto bajo la licencia [MIT](https://github.com/gofiber/fiber/blob/master/LICENSE). El logo oficial fuรฉ creado por [Vic Shรณstak](https://github.com/koddr) y distribuido bajo la licencia [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) (CC BY-SA 4.0 International). - -**Third-party library licenses** -- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) -- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) -- [fasttemplate](https://github.com/valyala/fasttemplate/blob/master/LICENSE) -- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) diff --git a/.github/README_fr.md b/.github/README_fr.md deleted file mode 100644 index f8a89ce1..00000000 --- a/.github/README_fr.md +++ /dev/null @@ -1,591 +0,0 @@ -

- - Fiber - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -

-

- Fiber est un framework web inspirรฉ d' Express. Il se base sur Fasthttp, l'implรฉmentation HTTP de Go la plus rapide. Conรงu pour faciliter les choses pour des dรฉveloppements rapides, Fiber garde ร  l'esprit l'absence d'allocations mรฉmoires, ainsi que les performances. -

- -## โšก๏ธ Quickstart - -```go -package main - -import "github.com/gofiber/fiber" - -func main() { - app := fiber.New() - - app.Get("/", func(c *fiber.Ctx) { - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -## ๐Ÿค– Benchmarks - -Ces tests sont effectuรฉs par [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) et [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Si vous voulez voir tous les rรฉsultats, n'hรฉsitez pas ร  consulter notre [Wiki](https://docs.gofiber.io/benchmarks). - -

- - -

- -## โš™๏ธ Installation - -Premiรจrement, [tรฉlรฉchargez](https://golang.org/dl/) et installez Go. Version `1.11` ou supรฉrieur requise. - -L'installation est ensuite lancรฉe via la commande [`go get`](https://golang.org/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them): - -```bash -go get -u github.com/gofiber/fiber/... -``` - -## ๐ŸŽฏ Features - -- [Routing](https://docs.gofiber.io/routing) robuste -- Serve [static files](https://docs.gofiber.io/application#static) -- [Performances](https://docs.gofiber.io/benchmarks) extrรชmes -- [Faible empreinte mรฉmoire](https://docs.gofiber.io/benchmarks) -- [API endpoints](https://docs.gofiber.io/context) -- Middleware & [Next](https://docs.gofiber.io/context#next) support -- Programmation cรดtรฉ serveur [rapide](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) -- [Template engines](https://github.com/gofiber/template) -- [WebSocket support](https://docs.gofiber.io/middleware#websocket) -- [Rate Limiter](https://docs.gofiber.io/middleware#limiter) -- Available in [15 languages](https://docs.gofiber.io/) -- Et plus encore, [explorez Fiber](https://docs.gofiber.io/) - -## ๐Ÿ’ก Philosophie - -Les nouveaux gophers qui passent de [Node.js](https://nodejs.org/en/about/) ร  [Go](https://golang.org/doc/) sont confrontรฉs ร  une courbe d'apprentissage, avant de pouvoir construire leurs applications web et microservices. Fiber, en tant que **framework web**, a รฉtรฉ mis au point avec en tรชte l'idรฉe de **minimalisme**, tout en suivant l'**UNIX way**, afin que les nouveaux gophers puissent rapidement entrer dans le monde de Go, avec un accueil chaleureux, de confiance. - -Fiber est **inspirรฉ** par Express, le framework web le plus populaire d'Internet. Nous avons combinรฉ la **facilitรฉ** d'Express, et la **performance brute** de Go. Si vous avez dรฉja dรฉveloppรฉ une application web en Node.js (_en utilisant Express ou รฉquivalent_), alors de nombreuses mรฉthodes et principes vous sembleront **familiers**. - -## ๐Ÿ‘€ Exemples - -Ci-dessous quelques exemples courants. Si vous voulez voir plus d'exemples, rendez-vous sur notre ["Recipes repository"](https://github.com/gofiber/recipes) ou visitez notre [documentation API](https://docs.gofiber.io). - -Listed below are some of the common examples. If you want to see more code examples , please visit our [Recipes repository](https://github.com/gofiber/recipes) or visit our hosted [API documentation](https://docs.gofiber.io). - -#### ๐Ÿ“– [**Basic Routing**](https://docs.gofiber.io/#basic-routing) - -```go -func main() { - app := fiber.New() - - // GET /john - app.Get("/:name", func(c *fiber.Ctx) { - msg := fmt.Sprintf("Hello, %s ๐Ÿ‘‹!", c.Params("name")) - c.Send(msg) // => Hello john ๐Ÿ‘‹! - }) - - // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ‘ด %s is %s years old", c.Params("name"), c.Params("age")) - c.Send(msg) // => ๐Ÿ‘ด john is 75 years old - }) - - // GET /dictionary.txt - app.Get("/:file.:ext", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ“ƒ %s.%s", c.Params("file"), c.Params("ext")) - c.Send(msg) // => ๐Ÿ“ƒ dictionary.txt - }) - - // GET /flights/LAX-SFO - app.Get("/flights/:from-:to", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ’ธ From: %s, To: %s", c.Params("from"), c.Params("to")) - c.Send(msg) // => ๐Ÿ’ธ From: LAX, To: SFO - }) - - // GET /api/register - app.Get("/api/*", func(c *fiber.Ctx) { - msg := fmt.Sprintf("โœ‹ %s", c.Params("*")) - c.Send(msg) // => โœ‹ /api/register - }) - - app.Listen(3000) -} -``` - -#### ๐Ÿ“– [**Serving Static Files**](https://docs.gofiber.io/application#static) - -```go -func main() { - app := fiber.New() - - app.Static("/", "./public") - // => http://localhost:3000/js/script.js - // => http://localhost:3000/css/style.css - - app.Static("/prefix", "./public") - // => http://localhost:3000/prefix/js/script.js - // => http://localhost:3000/prefix/css/style.css - - app.Static("*", "./public/index.html") - // => http://localhost:3000/any/path/shows/index/html - - app.Listen(3000) -} -``` - -#### ๐Ÿ“– [**Middleware & Next**](https://docs.gofiber.io/context#next) - -```go -func main() { - app := fiber.New() - - // Match any route - app.Use(func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‡ First handler") - c.Next() - }) - - // Match all routes starting with /api - app.Use("/api", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅˆ Second handler") - c.Next() - }) - - // GET /api/register - app.Get("/api/list", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‰ Last handler") - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -
- ๐Ÿ“š Show more code examples - -### Views engines - -๐Ÿ“– [Settings](https://docs.gofiber.io/application#settings) -๐Ÿ“– [Engines](https://github.com/gofiber/template) -๐Ÿ“– [Render](https://docs.gofiber.io/context#render) - -Fiber defaults to the [html/template](https://golang.org/pkg/html/template/) when no view engine is set. - -If you want to execute partials or use a different engine like [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) or [pug](https://github.com/Joker/jade) etc.. - -Checkout our [Template](https://github.com/gofiber/template) package that support multiple view engines. - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/template/pug" -) - -func main() { - // You can setup Views engine before initiation app: - app := fiber.New(&fiber.Settings{ - Views: pug.New("./views", ".pug"), - }) - - // OR after initiation app at any convenient location: - app.Settings.Views = pug.New("./views", ".pug"), - - // And now, you can call template `./views/home.pug` like this: - app.Get("/", func(c *fiber.Ctx) { - c.Render("home", fiber.Map{ - "title": "Homepage", - "year": 1999, - }) - }) - - // ... -} -``` - -### Grouping routes into chains - -๐Ÿ“– [Group](https://docs.gofiber.io/application#group) - -```go -func main() { - app := fiber.New() - - // Root API route - api := app.Group("/api", cors()) // /api - - // API v1 routes - v1 := api.Group("/v1", mysql()) // /api/v1 - v1.Get("/list", handler) // /api/v1/list - v1.Get("/user", handler) // /api/v1/user - - // API v2 routes - v2 := api.Group("/v2", mongodb()) // /api/v2 - v2.Get("/list", handler) // /api/v2/list - v2.Get("/user", handler) // /api/v2/user - - // ... -} -``` - -### Middleware logger - -๐Ÿ“– [Logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - // Default - app.Use(middleware.Logger()) - - // Custom logging format - app.Use(middleware.Logger("${method} - ${path}")) - - // Custom Config - app.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ - Next: func(ctx *fiber.Ctx) bool { - return ctx.Path() != "/private" - }, - Format: "${method} - ${path}", - Output: io.Writer, - })) - - app.Listen(3000) -} -``` - -### Cross-Origin Resource Sharing (CORS) - -๐Ÿ“– [CORS](https://docs.gofiber.io/middleware#cors) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/cors" -) - -func main() { - app := fiber.New() - - // CORS with default config - app.Use(cors.New()) - - app.Listen(3000) -} -``` - -Check CORS by passing any domain in `Origin` header: - -```bash -curl -H "Origin: http://example.com" --verbose http://localhost:3000 -``` - -### Custom 404 response - -๐Ÿ“– [HTTP Methods](https://docs.gofiber.io/application#http-methods) - -```go -func main() { - app := fiber.New() - - app.Static("./public") - - app.Get("/demo", func(c *fiber.Ctx) { - c.Send("This is a demo!") - }) - - app.Post("/register", func(c *fiber.Ctx) { - c.Send("Welcome!") - }) - - // Last middleware to match anything - app.Use(func(c *fiber.Ctx) { - c.SendStatus(404) - // => 404 "Not Found" - }) - - app.Listen(3000) -} -``` - -### JSON Response - -๐Ÿ“– [JSON](https://docs.gofiber.io/context#json) - -```go -type User struct { - Name string `json:"name"` - Age int `json:"age"` -} - -func main() { - app := fiber.New() - - app.Get("/user", func(c *fiber.Ctx) { - c.JSON(&User{"John", 20}) - // => {"name":"John", "age":20} - }) - - app.Get("/json", func(c *fiber.Ctx) { - c.JSON(fiber.Map{ - "success": true, - "message": "Hi John!", - }) - // => {"success":true, "message":"Hi John!"} - }) - - app.Listen(3000) -} -``` - -### WebSocket Upgrade - -๐Ÿ“– [Websocket](https://docs.gofiber.io/middleware#websocket) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/websocket" -) - -func main() { - app := fiber.New() - - app.Get("/ws", websocket.New(func(c *websocket.Conn) { - for { - mt, msg, err := c.ReadMessage() - if err != nil { - log.Println("read:", err) - break - } - log.Printf("recv: %s", msg) - err = c.WriteMessage(mt, msg) - if err != nil { - log.Println("write:", err) - break - } - } - })) - - app.Listen(3000) - // ws://localhost:3000/ws -} -``` - -### Recover middleware - -๐Ÿ“– [Recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - app.Use(middleware.Recover()) - - app.Get("/", func(c *fiber.Ctx) { - panic("normally this would crash your app") - }) - - app.Listen(3000) -} -``` -
- -## ๐Ÿงฌ Fiber Middleware - -The Fiber middleware modules listed here are maintained by the [Fiber team](https://github.com/orgs/gofiber/people). - -| Middleware | Description | Built-in middleware | -| :--- | :--- | :--- | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | - | -| [basicauth](https://github.com/gofiber/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | - | -| [compress](https://github.com/gofiber/fiber/blob/master/middleware/compress.md) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | `middleware.Compress()` | -| [cors](https://github.com/gofiber/cors) | Enable cross-origin resource sharing \(CORS\) with various options. | - | -| [csrf](https://github.com/gofiber/csrf) | Protect from CSRF exploits. | - | -| [filesystem](https://github.com/gofiber/fiber/blob/master/middleware/filesystem.md) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | - | -| [favicon](https://github.com/gofiber/fiber/blob/master/middleware/favicon.md) | Ignore favicon from logs or serve from memory if a file path is provided. | `middleware.Favicon()` | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | - | -| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | - | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | - | -| [limiter](https://github.com/gofiber/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | - | -| [logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) | HTTP request/response logger. | `middleware.Logger()` | -| [pprof](https://github.com/gofiber/pprof) | Special thanks to Matthew Lee \(@mthli\) | - | -| [recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) | Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized[ ErrorHandler](error-handling.md). | `middleware.Recover()` | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | - | -| [requestid](https://github.com/gofiber/fiber/blob/master/middleware/request_id.md) | Request ID middleware generates a unique id for a request. | `middleware.RequestID()` | -| [session](https://github.com/gofiber/session) | This session middleware is build on top of fasthttp/session by @savsgio MIT. Special thanks to @thomasvvugt for helping with this middleware. | - | -| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | - | -| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | - | - -## ๐ŸŒฑ Third Party Middlewares - -This is a list of middlewares that are created by the Fiber community, please create a PR if you want to see yours! -- [arsmn/fiber-casbin](https://github.com/arsmn/fiber-casbin) -- [arsmn/fiber-introspect](https://github.com/arsmn/fiber-introspect) -- [arsmn/fiber-swagger](https://github.com/arsmn/fiber-swagger) -- [arsmn/gqlgen](https://github.com/arsmn/gqlgen) -- [codemicro/fiber-cache](https://github.com/codemicro/fiber-cache) -- [itsursujit/fiber-boilerplate](https://github.com/itsursujit/fiber-boilerplate) -- [juandiii/go-jwk-security](https://github.com/juandiii/go-jwk-security) -- [kiyonlin/fiber_limiter](https://github.com/kiyonlin/fiber_limiter) -- [shareed2k/fiber_limiter](https://github.com/shareed2k/fiber_limiter) -- [shareed2k/fiber_tracing](https://github.com/shareed2k/fiber_tracing) -- [thomasvvugt/fiber-boilerplate](https://github.com/thomasvvugt/fiber-boilerplate) - -## ๐Ÿ’ฌ Media - -

- - -

- -- [Welcome to Fiber โ€” an Express.js styled web framework written in Go with โค๏ธ](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) โ€” _03 Feb 2020_ -- [Fiber released v1.7! ๐ŸŽ‰ What's new and is it still fast, flexible and friendly?](https://dev.to/koddr/fiber-v2-is-out-now-what-s-new-and-is-he-still-fast-flexible-and-friendly-3ipf) โ€” _21 Feb 2020_ -- [๐Ÿš€ Fiber v1.8. What's new, updated and re-thinked?](https://dev.to/koddr/fiber-v1-8-what-s-new-updated-and-re-thinked-339h) โ€” _03 Mar 2020_ -- [Is switching from Express to Fiber worth it? ๐Ÿค”](https://dev.to/koddr/are-sure-what-your-lovely-web-framework-running-so-fast-2jl1) โ€” _01 Apr 2020_ -- [Creating Fast APIs In Go Using Fiber](https://dev.to/jozsefsallai/creating-fast-apis-in-go-using-fiber-59m9) โ€” _07 Apr 2020_ -- [Building a Basic REST API in Go using Fiber](https://tutorialedge.net/golang/basic-rest-api-go-fiber/) - _23 Apr 2020_ -- [๐Ÿ“บ Building a REST API using GORM and Fiber](https://youtu.be/Iq2qT0fRhAA) - _25 Apr 2020_ -- [๐ŸŒŽ Create a travel list app with Go, Fiber, Angular, MongoDB and Google Cloud Secret Manager](https://blog.yongweilun.me/create-a-travel-list-app-with-go-fiber-angular-mongodb-and-google-cloud-secret-manager-ck9fgxy0p061pcss1xt1ubu8t) - _25 Apr 2020_ -- [Fiber v1.9.6 ๐Ÿ”ฅ How to improve performance by 817% and stay fast, flexible and friendly?](https://dev.to/koddr/fiber-v1-9-5-how-to-improve-performance-by-817-and-stay-fast-flexible-and-friendly-2dp6) - _12 May 2020_ -- [The road to web-based authentication with Fiber โšก](https://vugt.me/the-road-to-web-based-authentication-with-fiber/) - _20 May 2020_ -- [Building an Express-style API in Go with Fiber](https://blog.logrocket.com/express-style-api-go-fiber/) - _10 June 2020_ -- [ๅŸบไบŽgolang fiberๅ’Œangularๅผ€ๅ‘web](https://zhuanlan.zhihu.com/p/148925642) - _19 June 2020_ -- [ๅŸบไบŽๅปถ่ฟŸ่ฎก็ฎ—ไปค็‰Œๆกถ็š„gofiber้ข‘็Ž‡้™ๅˆถไธญ้—ดไปถๅฎž็Žฐ](https://zhuanlan.zhihu.com/p/149308936) - _20 June 2020_ -- [Construir una API en Golang con Fiber ๐Ÿ‡ช๐Ÿ‡ธ](https://enbonnet.me/article/53/construir-api-golang-con-fiber) - _28 June 2020_ -- [๐Ÿ“บWhy Go Fiber Is THE New Framework To Learn](https://www.youtube.com/watch?v=kvwsPeWDLM8) - _29 June 2020_ -- [่งฃๆžGofiber่ทฏ็”ฑ็ฎก็†](https://zhuanlan.zhihu.com/p/152494502) - _08 July 2020_ -- [๐Ÿ“บ Introduction to Fiber - An Express-inspired web framework](https://youtu.be/MfFi4Gt-tos) - _25 July 2020_ - -## ๐Ÿ‘ Contribuer - -Si vous voulez nous remercier et/ou soutenir le dรฉveloppement actif de `Fiber`: - -1. Ajoutez une [GitHub Star](https://github.com/gofiber/fiber/stargazers) ร  ce projet. -2. Twittez ร  propos de ce projet [sur votre Twitter](https://twitter.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). -3. Ecrivez un article (review, tutorial) sur [Medium](https://medium.com/), [Dev.to](https://dev.to/), ou encore un blog personnel. -4. Help us to translate our API Documentation via [Crowdin](https://crowdin.com/project/gofiber) [![Crowdin](https://badges.crowdin.net/gofiber/localized.svg)](https://crowdin.com/project/gofiber) -5. Support the project by donating a [cup of coffee](https://buymeacoff.ee/fenny). - -## โ˜• Supporters - -Fiber is an open source project that runs on donations to pay the bills e.g. our domain name, gitbook, netlify and serverless hosting. If you want to support Fiber, you can โ˜• [**buy a coffee here**](https://buymeacoff.ee/fenny). - -| | User | Donation | -| :---------------------------------------------------------- | :----------------------------------------------- | :-------- | -| ![](https://avatars.githubusercontent.com/u/204341?s=25 ) | [@destari](https://github.com/destari) | โ˜• x 10 | -| ![](https://avatars.githubusercontent.com/u/63164982?s=25 ) | [@dembygenesis](https://github.com/dembygenesis) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/56607882?s=25 ) | [@thomasvvugt](https://github.com/thomasvvugt) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/27820675?s=25 ) | [@hendratommy](https://github.com/hendratommy) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/1094221?s=25 ) | [@ekaputra07](https://github.com/ekaputra07) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/194590?s=25 ) | [@jorgefuertes](https://github.com/jorgefuertes) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/186637?s=25 ) | [@candidosales](https://github.com/candidosales) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/29659953?s=25 ) | [@l0nax](https://github.com/l0nax) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/59947262?s=25 ) | [@ankush](https://github.com/ankush) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/635852?s=25 ) | [@bihe](https://github.com/bihe) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/307334?s=25 ) | [@justdave](https://github.com/justdave) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/11155743?s=25 ) | [@koddr](https://github.com/koddr) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/29042462?s=25 ) | [@lapolinar](https://github.com/lapolinar) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/2978730?s=25 ) | [@diegowifi](https://github.com/diegowifi) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/44171355?s=25 ) | [@ssimk0](https://github.com/ssimk0) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/5638101?s=25 ) | [@raymayemir](https://github.com/raymayemir) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/619996?s=25 ) | [@melkorm](https://github.com/melkorm) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31022056?s=25 ) | [@marvinjwendt](https://github.com/thomasvvugt) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31921460?s=25 ) | [@toishy](https://github.com/toishy) | โ˜• x 1 | - -## โ€Žโ€๐Ÿ’ป Code Contributors - -Code Contributors - -## โญ๏ธ Stargazers - -Stargazers over time - -## โš ๏ธ License - -Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` is free and open-source software licensed under the [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Official logo was created by [Vic Shรณstak](https://github.com/koddr) and distributed under [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) license (CC BY-SA 4.0 International). - -**Third-party library licenses** -- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) -- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) -- [fasttemplate](https://github.com/valyala/fasttemplate/blob/master/LICENSE) -- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) diff --git a/.github/README_he.md b/.github/README_he.md deleted file mode 100644 index db5295c7..00000000 --- a/.github/README_he.md +++ /dev/null @@ -1,738 +0,0 @@ -

- - Fiber - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -

-

-

- - Fiber ื”ื™ื web framework ื‘ื”ืฉืจืืช Express ื”ื‘ื ื•ื™ื” ืขืœ ื’ื‘ื™ Fasthttp, ืžื ื•ืข ื”-HTTP ื”ืžื”ื™ืจ ื‘ื™ื•ืชืจ ืขื‘ื•ืจ Go. - ื ื•ืขื“ื” ืœื”ืงืœ ืขืœ ื”ืขื ื™ื™ื ื™ื ืœืžืขืŸ ืคื™ืชื•ื— ืžื”ื™ืจ, ืœืœื ื”ืงืฆืื•ืช ื–ื›ืจื•ืŸ ื•ืœื•ืงื—ืช ื‘ื™ืฆื•ืขื™ื ื‘ื—ืฉื‘ื•ืŸ. -
-

- -
- -## โšก๏ธ ื”ืชื—ืœื” ืžื”ื™ืจื” -
- -```go -package main - -import "github.com/gofiber/fiber" - -func main() { - app := fiber.New() - - app.Get("/", func(c *fiber.Ctx) { - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -
- -## ๐Ÿค– ืžื“ื“ื™ื -
- -
- -ื”ื‘ื“ื™ืงื•ืช ืžื‘ื•ืฆืขื•ืช ืขืœ ื™ื“ื™ [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) ื•-[Go Web](https://github.com/smallnest/go-web-framework-benchmark). ืื ืืชื ืจื•ืฆื™ื ืœืจืื•ืช ืืช ื›ืœ ื”ืชื•ืฆืื•ืช, ืื ื ื‘ืงืจื• ื‘-[Wiki](https://docs.gofiber.io/benchmarks) ืฉืœื ื•. -
- -

- - -

- -
- -## โš™๏ธ ื”ืชืงื ื” -
- -
- -ืงื•ื“ื ื›ืœ, [ื”ื•ืจื™ื“ื•](https://golang.org/dl/) ื•ื”ืชืงื™ื ื• ืืช Go. ื ื“ืจืฉืช ื’ืจืกื” `1.11` ื•ืžืขืœื”. -
- -
- -ื”ื”ืชืงื ื” ืžืชื‘ืฆืขืช ื‘ืืžืฆืขื•ืช ื”ืคืงื•ื“ื” [`go get`](https://golang.org/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them): -
- -```bash -go get -u github.com/gofiber/fiber -``` - -
- -## ๐ŸŽฏ ื™ื›ื•ืœื•ืช -
- -
- -- [ื ื™ืชื•ื‘](https://docs.gofiber.io/routing) ืจื•ื‘ืกื˜ื™ -- ื”ื ื’ืฉืช [ืงื‘ืฆื™ื ืกื˜ื˜ื™ื™ื](https://docs.gofiber.io/application#static) -- [ื‘ื™ืฆื•ืขื™ื](https://docs.gofiber.io/benchmarks) ื’ื‘ื•ื”ื™ื ื‘ืžื™ื•ื—ื“ -- ืฆื•ืจืš ื›ืžื•ืช [ื–ื›ืจื•ืŸ ืงื˜ื ื”](https://docs.gofiber.io/benchmarks) -- [ื ืงื•ื“ื•ืช ืงืฆื” ืขื‘ื•ืจ API](https://docs.gofiber.io/context) -- ืชืžื™ื›ื” ื‘-[Middleware](https://docs.gofiber.io/middleware) & [Next](https://docs.gofiber.io/context#next) -- ืชื›ื ื•ืช [ืžื”ื™ืจ](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) ืฉืœ ืฆื“ ืฉืจืช -- [ืžื ื•ืขื™ ืชื‘ื ื™ื•ืช](https://docs.gofiber.io/middleware#template) -- [ืชืžื™ื›ื” ื‘-WebSocket](https://docs.gofiber.io/middleware#websocket) -- [ื”ื’ื‘ืœืช ืงืฆื‘ื™ื ื•ื‘ืงืฉื•ืช](https://docs.gofiber.io/middleware#limiter) -- Available in [12 languages](https://docs.gofiber.io/) -- ื•ื”ืจื‘ื” ื™ื•ืชืจ, [ื—ืงื•ืจ ืืช Fiber](https://docs.gofiber.io/) -
- -
- -## ๐Ÿ’ก ืคื™ืœื•ืกื•ืคื™ื” -
- -
- -gophers ื—ื“ืฉื™ื ืฉืขื•ืฉื™ื ืืช ื”ืžืขื‘ืจ ืž-[Node.js](https://nodejs.org/en/about/) ืœ-[Go](https://golang.org/doc/) ืžืชืžื•ื“ื“ื™ื ืขื ืขืงื•ืžืช ืœืžื™ื“ื” ืœืคื ื™ ืฉื”ื ื™ื›ื•ืœื™ื ืœื”ืชื—ื™ืœ ืœื‘ื ื•ืช ืืช ื™ื™ืฉื•ืžื™ ื”ืื™ื ื˜ืจื ื˜ ืื• ื”ืžื™ืงืจื•-ืฉื™ืจื•ืชื™ื ืฉืœื”ื. -Fiber ื›-**web framework**, ื ื•ืฆืจื” ืขื ืจืขื™ื•ืŸ **ื”ืžื™ื ื™ืžืœื™ื–ื** ื•ืขื•ืงื‘ืช ืื—ืจื™ **ื”ื“ืจืš ืฉืœ UNIX**, ื›ืš ืฉ-gophers ื—ื“ืฉื™ื ื™ื•ื›ืœื• ืœื”ื™ื›ื ืก ื‘ืžื”ื™ืจื•ืช ืœืขื•ืœื ืฉืœ Go ืขื ืงื‘ืœืช ืคื ื™ื ื—ืžื” ื•ืืžื™ื ื”. -
- -
- -Fiber ื ื•ืฆืจื” **ื‘ื”ืฉืจืืช** Express, ื”-web framework ื”ืคื•ืคื•ืœืจื™ืช ื‘ื™ื•ืชืจ ื‘ืจื—ื‘ื™ ื”ืื™ื ื˜ืจื ื˜. ืฉื™ืœื‘ื ื• ืืช **ื”ืงืœื•ืช** ืฉืœ Express ื•**ื”ื‘ื™ืฆื•ืขื™ื ื”ื’ื•ืœืžื™ื™ื** ืฉืœ Go. ืื ืื™-ืคืขื ืžื™ืžืฉืชื ื™ื™ืฉื•ื web ื‘-Node.js (_ื‘ืืžืฆืขื•ืช Express ืื• ื“ื•ืžื™ื•_), ืื– ื”ืจื‘ื” ืžื”ืคื•ื ืงืฆื™ื•ืช ื•ื”ืขืงืจื•ื ื•ืช ื™ื™ืจืื• ืœื›ื **ืžืื•ื“ ืžื•ื›ืจื™ื**. -
- -
- -ืื ื—ื ื• **ืžืงืฉื™ื‘ื™ื** ืœืžืฉืชืžืฉื™ื ืฉืœื ื• ื‘-[issues](https://github.com/gofiber/fiber/issues) (_ื•ื‘ื›ืœ ืจื—ื‘ื™ ื”ืื™ื ื˜ืจื ื˜_) ื›ื“ื™ ืœื™ืฆื•ืจ web framework **ืžื”ื™ืจื”**, **ื’ืžื™ืฉื”**, ื•**ื™ื“ื™ื“ื•ืชื™ืช** ื‘ืฉืคืช Go ืขื‘ื•ืจ **ื›ืœ** ืžืฉื™ืžื”, **ืชืืจื™ืš ื™ืขื“** ื•**ื›ื™ืฉื•ืจื™** ืžืคืชื—! ื‘ื“ื™ื•ืง ื›ืžื• ืฉ-Express ืžื‘ืฆืข ื‘ืขื•ืœื ืฉืœ JavaScript. -
- -
- -## ๐Ÿ‘€ ื“ื•ื’ืžืื•ืช -
- -
- -ืœื”ืœืŸ ื›ืžื” ืžื”ื“ื•ื’ืžืื•ืช ื”ื ืคื•ืฆื•ืช. ืื ื‘ืจืฆื•ื ื›ื ืœืจืื•ืช ื“ื•ื’ืžืื•ืช ืงื•ื“ ื ื•ืกืคื•ืช, ืื ื ื‘ืงืจื• ื‘[ืžืื’ืจ ื”ืžืชื›ื•ื ื™ื](https://github.com/gofiber/recipes) ืฉืœื ื• ืื• ื‘ืงืจื• ื‘[ืชื™ืขื•ื“ ื”-API](https://docs.gofiber.io) ืฉืœื ื•. -
- - -
- -#### ๐Ÿ“– [**Basic Routing**](https://docs.gofiber.io/#basic-routing) -
- -```go -func main() { - app := fiber.New() - - // GET /john - app.Get("/:name", func(c *fiber.Ctx) { - msg := fmt.Sprintf("Hello, %s ๐Ÿ‘‹!", c.Params("name")) - c.Send(msg) // => Hello john ๐Ÿ‘‹! - }) - - // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ‘ด %s is %s years old", c.Params("name"), c.Params("age")) - c.Send(msg) // => ๐Ÿ‘ด john is 75 years old - }) - - // GET /dictionary.txt - app.Get("/:file.:ext", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ“ƒ %s.%s", c.Params("file"), c.Params("ext")) - c.Send(msg) // => ๐Ÿ“ƒ dictionary.txt - }) - - // GET /flights/LAX-SFO - app.Get("/flights/:from-:to", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ’ธ From: %s, To: %s", c.Params("from"), c.Params("to")) - c.Send(msg) // => ๐Ÿ’ธ From: LAX, To: SFO - }) - - // GET /api/register - app.Get("/api/*", func(c *fiber.Ctx) { - msg := fmt.Sprintf("โœ‹ %s", c.Params("*")) - c.Send(msg) // => โœ‹ /api/register - }) - - app.Listen(3000) -} -``` - -
- -#### ๐Ÿ“– [**Serving Static Files**](https://docs.gofiber.io/application#static) -
- -```go -func main() { - app := fiber.New() - - app.Static("/", "./public") - // => http://localhost:3000/js/script.js - // => http://localhost:3000/css/style.css - - app.Static("/prefix", "./public") - // => http://localhost:3000/prefix/js/script.js - // => http://localhost:3000/prefix/css/style.css - - app.Static("*", "./public/index.html") - // => http://localhost:3000/any/path/shows/index/html - - app.Listen(3000) -} -``` - -
- -#### ๐Ÿ“– [**Middleware & Next**](https://docs.gofiber.io/context#next) -
- -```go -func main() { - app := fiber.New() - - // Match any route - app.Use(func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‡ First handler") - c.Next() - }) - - // Match all routes starting with /api - app.Use("/api", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅˆ Second handler") - c.Next() - }) - - // GET /api/register - app.Get("/api/list", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‰ Last handler") - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -
-
- ๐Ÿ“š ื”ืฆื’ ื“ื•ื’ืžืื•ืช ืงื•ื“ ื ื•ืกืคื•ืช - - -### Views engines - -๐Ÿ“– [Settings](https://docs.gofiber.io/application#settings) -๐Ÿ“– [Engines](https://github.com/gofiber/template) -๐Ÿ“– [Render](https://docs.gofiber.io/context#render) - -Fiber defaults to the [html/template](https://golang.org/pkg/html/template/) when no view engine is set. - -If you want to execute partials or use a different engine like [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) or [pug](https://github.com/Joker/jade) etc.. - -Checkout our [Template](https://github.com/gofiber/template) package that support multiple view engines. - -
- -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/template/pug" -) - -func main() { - // You can setup Views engine before initiation app: - app := fiber.New(&fiber.Settings{ - Views: pug.New("./views", ".pug"), - }) - - // OR after initiation app at any convenient location: - app.Settings.Views = pug.New("./views", ".pug"), - - // And now, you can call template `./views/home.pug` like this: - app.Get("/", func(c *fiber.Ctx) { - c.Render("home", fiber.Map{ - "title": "Homepage", - "year": 1999, - }) - }) - - // ... -} -``` -
- -### ืงื™ื‘ื•ืฅ routes ืœ-chains - -๐Ÿ“– [ืงื‘ื•ืฆื•ืช](https://docs.gofiber.io/application#group) - -
- -```go -func main() { - app := fiber.New() - - // Root API route - api := app.Group("/api", cors()) // /api - - // API v1 routes - v1 := api.Group("/v1", mysql()) // /api/v1 - v1.Get("/list", handler) // /api/v1/list - v1.Get("/user", handler) // /api/v1/user - - // API v2 routes - v2 := api.Group("/v2", mongodb()) // /api/v2 - v2.Get("/list", handler) // /api/v2/list - v2.Get("/user", handler) // /api/v2/user - - // ... -} -``` -
- -### Middleware ืฉืœ ืœื•ื’ื™ื - -๐Ÿ“– [Logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) - -
- -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - // Default - app.Use(middleware.Logger()) - - // Custom logging format - app.Use(middleware.Logger("${method} - ${path}")) - - // Custom Config - app.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ - Next: func(ctx *fiber.Ctx) bool { - return ctx.Path() != "/private" - }, - Format: "${method} - ${path}", - Output: io.Writer, - })) - - app.Listen(3000) -} -``` - -
- -### ืฉื™ืชื•ืฃ ืžืฉืื‘ื™ื ื‘ื™ืŸ ืžืงื•ืจื•ืช (CORS) - -๐Ÿ“– [CORS](https://docs.gofiber.io/middleware#cors) - -
- -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/cors" -) - -func main() { - app := fiber.New() - - // CORS with default config - app.Use(cors.New()) - - app.Listen(3000) -} -``` -
- -ื‘ื“ื•ืง ืืช ื”-CORS ืขืœ ื™ื“ื™ ื”ืขื‘ืจืช ื›ืœ domain ื‘-header ืฉืœ `Origin`: - -
- -```bash -curl -H "Origin: http://example.com" --verbose http://localhost:3000 -``` -
- -### ืชื’ื•ื‘ืช 404 ืžื•ืชืืžืช ืื™ืฉื™ืช - -๐Ÿ“– [ืฉื™ื˜ื•ืช HTTP](https://docs.gofiber.io/application#http-methods) - -
- -```go -func main() { - app := fiber.New() - - app.Static("./public") - - app.Get("/demo", func(c *fiber.Ctx) { - c.Send("This is a demo!") - }) - - app.Post("/register", func(c *fiber.Ctx) { - c.Send("Welcome!") - }) - - // Last middleware to match anything - app.Use(func(c *fiber.Ctx) { - c.SendStatus(404) - // => 404 "Not Found" - }) - - app.Listen(3000) -} -``` -
- -### ืชื’ื•ื‘ืช JSON - -๐Ÿ“– [JSON](https://docs.gofiber.io/context#json) - -
- -```go -type User struct { - Name string `json:"name"` - Age int `json:"age"` -} - -func main() { - app := fiber.New() - - app.Get("/user", func(c *fiber.Ctx) { - c.JSON(&User{"John", 20}) - // => {"name":"John", "age":20} - }) - - app.Get("/json", func(c *fiber.Ctx) { - c.JSON(fiber.Map{ - "success": true, - "message": "Hi John!", - }) - // => {"success":true, "message":"Hi John!"} - }) - - app.Listen(3000) -} -``` -
- -### WebSocket Upgrade - -๐Ÿ“– [Websocket](https://docs.gofiber.io/middleware#websocket) - -
- -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/websocket" -) - -func main() { - app := fiber.New() - - app.Get("/ws", websocket.New(func(c *websocket.Conn) { - for { - mt, msg, err := c.ReadMessage() - if err != nil { - log.Println("read:", err) - break - } - log.Printf("recv: %s", msg) - err = c.WriteMessage(mt, msg) - if err != nil { - log.Println("write:", err) - break - } - } - })) - - app.Listen(3000) - // ws://localhost:3000/ws -} -``` -
- -### Middleware ืฉืœ ื”ืชืื•ืฉืฉื•ืช - -๐Ÿ“– [ื”ืชืื•ืฉืฉื•ืช](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) - -
- -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - app.Use(middleware.Recover()) - - app.Get("/", func(c *fiber.Ctx) { - panic("normally this would crash your app") - }) - - app.Listen(3000) -} -``` -
-
-
- -
- -## ๐Ÿงฌ Fiber Middleware -
- -
- -The Fiber middleware modules listed here are maintained by the [Fiber team](https://github.com/orgs/gofiber/people). -
- -
- -| Middleware | Description | Built-in middleware | -| :--- | :--- | :--- | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | - | -| [basicauth](https://github.com/gofiber/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | - | -| [compress](https://github.com/gofiber/fiber/blob/master/middleware/compress.md) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | `middleware.Compress()` | -| [cors](https://github.com/gofiber/cors) | Enable cross-origin resource sharing \(CORS\) with various options. | - | -| [csrf](https://github.com/gofiber/csrf) | Protect from CSRF exploits. | - | -| [filesystem](https://github.com/gofiber/fiber/blob/master/middleware/filesystem.md) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | - | -| [favicon](https://github.com/gofiber/fiber/blob/master/middleware/favicon.md) | Ignore favicon from logs or serve from memory if a file path is provided. | `middleware.Favicon()` | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | - | -| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | - | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | - | -| [limiter](https://github.com/gofiber/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | - | -| [logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) | HTTP request/response logger. | `middleware.Logger()` | -| [pprof](https://github.com/gofiber/pprof) | Special thanks to Matthew Lee \(@mthli\) | - | -| [recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) | Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized[ ErrorHandler](error-handling.md). | `middleware.Recover()` | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | - | -| [requestid](https://github.com/gofiber/fiber/blob/master/middleware/request_id.md) | Request ID middleware generates a unique id for a request. | `middleware.RequestID()` | -| [session](https://github.com/gofiber/session) | This session middleware is build on top of fasthttp/session by @savsgio MIT. Special thanks to @thomasvvugt for helping with this middleware. | - | -| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | - | -| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | - | - -
- -
- -## ๐ŸŒฑ Third Party Middlewares -
- -
- -This is a list of middlewares that are created by the Fiber community, please create a PR if you want to see yours! -
- -
- -- [arsmn/fiber-casbin](https://github.com/arsmn/fiber-casbin) -- [arsmn/fiber-introspect](https://github.com/arsmn/fiber-introspect) -- [arsmn/fiber-swagger](https://github.com/arsmn/fiber-swagger) -- [arsmn/gqlgen](https://github.com/arsmn/gqlgen) -- [codemicro/fiber-cache](https://github.com/codemicro/fiber-cache) -- [itsursujit/fiber-boilerplate](https://github.com/itsursujit/fiber-boilerplate) -- [juandiii/go-jwk-security](https://github.com/juandiii/go-jwk-security) -- [kiyonlin/fiber_limiter](https://github.com/kiyonlin/fiber_limiter) -- [shareed2k/fiber_limiter](https://github.com/shareed2k/fiber_limiter) -- [shareed2k/fiber_tracing](https://github.com/shareed2k/fiber_tracing) -- [thomasvvugt/fiber-boilerplate](https://github.com/thomasvvugt/fiber-boilerplate) -
- -
- -## ๐Ÿ’ฌ ืžื“ื™ื” -
- -
- -

- - -

- -- [Welcome to Fiber โ€” an Express.js styled web framework written in Go with โค๏ธ](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) โ€” _03 Feb 2020_ -- [Fiber released v1.7! ๐ŸŽ‰ What's new and is it still fast, flexible and friendly?](https://dev.to/koddr/fiber-v2-is-out-now-what-s-new-and-is-he-still-fast-flexible-and-friendly-3ipf) โ€” _21 Feb 2020_ -- [๐Ÿš€ Fiber v1.8. What's new, updated and re-thinked?](https://dev.to/koddr/fiber-v1-8-what-s-new-updated-and-re-thinked-339h) โ€” _03 Mar 2020_ -- [Is switching from Express to Fiber worth it? ๐Ÿค”](https://dev.to/koddr/are-sure-what-your-lovely-web-framework-running-so-fast-2jl1) โ€” _01 Apr 2020_ -- [Creating Fast APIs In Go Using Fiber](https://dev.to/jozsefsallai/creating-fast-apis-in-go-using-fiber-59m9) โ€” _07 Apr 2020_ -- [Building a Basic REST API in Go using Fiber](https://tutorialedge.net/golang/basic-rest-api-go-fiber/) - _23 Apr 2020_ -- [๐Ÿ“บ Building a REST API using GORM and Fiber](https://youtu.be/Iq2qT0fRhAA) - _25 Apr 2020_ -- [๐ŸŒŽ Create a travel list app with Go, Fiber, Angular, MongoDB and Google Cloud Secret Manager](https://blog.yongweilun.me/create-a-travel-list-app-with-go-fiber-angular-mongodb-and-google-cloud-secret-manager-ck9fgxy0p061pcss1xt1ubu8t) - _25 Apr 2020_ -- [Fiber v1.9.6 ๐Ÿ”ฅ How to improve performance by 817% and stay fast, flexible and friendly?](https://dev.to/koddr/fiber-v1-9-5-how-to-improve-performance-by-817-and-stay-fast-flexible-and-friendly-2dp6) - _12 May 2020_ -- [The road to web-based authentication with Fiber โšก](https://vugt.me/the-road-to-web-based-authentication-with-fiber/) - _20 May 2020_ -- [Building an Express-style API in Go with Fiber](https://blog.logrocket.com/express-style-api-go-fiber/) - _10 June 2020_ -- [ๅŸบไบŽgolang fiberๅ’Œangularๅผ€ๅ‘web](https://zhuanlan.zhihu.com/p/148925642) - _19 June 2020_ -- [ๅŸบไบŽๅปถ่ฟŸ่ฎก็ฎ—ไปค็‰Œๆกถ็š„gofiber้ข‘็Ž‡้™ๅˆถไธญ้—ดไปถๅฎž็Žฐ](https://zhuanlan.zhihu.com/p/149308936) - _20 June 2020_ -- [Construir una API en Golang con Fiber ๐Ÿ‡ช๐Ÿ‡ธ](https://enbonnet.me/article/53/construir-api-golang-con-fiber) - _28 June 2020_ -- [๐Ÿ“บWhy Go Fiber Is THE New Framework To Learn](https://www.youtube.com/watch?v=kvwsPeWDLM8) - _29 June 2020_ -- [่งฃๆžGofiber่ทฏ็”ฑ็ฎก็†](https://zhuanlan.zhihu.com/p/152494502) - _08 July 2020_ -- [๐Ÿ“บ Introduction to Fiber - An Express-inspired web framework](https://youtu.be/MfFi4Gt-tos) - _25 July 2020_ -
- -
- -## ๐Ÿ‘ ืœืชืจื•ื -
- -
- -ืื ืืชื ืจื•ืฆื™ื ืœื•ืžืจ **ืชื•ื“ื”** ืื•/ื• ืœืชืžื•ืš ื‘ืคื™ืชื•ื— ื”ืคืขื™ืœ ืฉืœ `Fiber`: - -
- -
- -1. ืชื•ืกื™ืคื• [GitHub Star](https://github.com/gofiber/fiber/stargazers) ืœืคืจื•ื™ืงื˜. -2. ืฆื™ื™ืฆื• ืœื’ื‘ื™ ื”ืคืจื•ื™ืงื˜ [ื‘ื˜ื•ื•ื™ื˜ืจ ืฉืœื›ื](https://twitter.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). -3. ื›ืชื‘ื• ื‘ื™ืงื•ืจืช ืื• ืžื“ืจื™ืš ื‘-[Medium](https://medium.com/), [Dev.to](https://dev.to/) ืื• ื‘ื‘ืœื•ื’ ื”ืื™ืฉื™ ืฉืœื›ื. -4. ืขื–ืจื• ืœื ื• ืœืชืจื’ื ืืช ื”-`README` ื”ื–ื” ืœืฉืคื” ืื—ืจืช. -5. ืชืžื›ื• ื‘ืคืจื•ื™ืงื˜ ืขืœ ื™ื“ื™ ืชืจื•ืžืช [ื›ื•ืก ืงืคื”](https://buymeacoff.ee/fenny). -
- - -
- -## โ˜• ืชื•ืžื›ื™ื -
- -
- -Fiber ื”ื™ื ืคืจื•ื™ืงื˜ ืงื•ื“ ืคืชื•ื— ืฉืชืฉืœื•ื ื—ืฉื•ื‘ื•ื ืชื™ื• ืžืกืชืžืš ืขืœ ืชืจื•ืžื•ืช, ื›ื’ื•ืŸ ืฉื ื”-domain ืฉืœื ื•, gitbook, netlify ื•-serverless hosting. ืื ืืชื ืจื•ืฆื™ื ืœืชืžื•ืš ื‘-Fiber, ืืชื ื™ื›ื•ืœื™ื โ˜• [**ืงื ื• ืงืคื” ื›ืืŸ**](https://buymeacoff.ee/fenny). -
- -| | ืžืฉืชืžืฉ | ืชืจื•ืžื” | -| :---------------------------------------------------------- | :----------------------------------------------- | :-------- | -| ![](https://avatars.githubusercontent.com/u/204341?s=25 ) | [@destari](https://github.com/destari) | โ˜• x 10 | -| ![](https://avatars.githubusercontent.com/u/63164982?s=25 ) | [@dembygenesis](https://github.com/dembygenesis) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/56607882?s=25 ) | [@thomasvvugt](https://github.com/thomasvvugt) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/27820675?s=25 ) | [@hendratommy](https://github.com/hendratommy) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/1094221?s=25 ) | [@ekaputra07](https://github.com/ekaputra07) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/194590?s=25 ) | [@jorgefuertes](https://github.com/jorgefuertes) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/186637?s=25 ) | [@candidosales](https://github.com/candidosales) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/29659953?s=25 ) | [@l0nax](https://github.com/l0nax) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/59947262?s=25 ) | [@ankush](https://github.com/ankush) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/635852?s=25 ) | [@bihe](https://github.com/bihe) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/307334?s=25 ) | [@justdave](https://github.com/justdave) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/11155743?s=25 ) | [@koddr](https://github.com/koddr) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/29042462?s=25 ) | [@lapolinar](https://github.com/lapolinar) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/2978730?s=25 ) | [@diegowifi](https://github.com/diegowifi) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/44171355?s=25 ) | [@ssimk0](https://github.com/ssimk0) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/5638101?s=25 ) | [@raymayemir](https://github.com/raymayemir) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/619996?s=25 ) | [@melkorm](https://github.com/melkorm) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31022056?s=25 ) | [@marvinjwendt](https://github.com/thomasvvugt) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31921460?s=25 ) | [@toishy](https://github.com/toishy) | โ˜• x 1 | - - -
- -## โ€Žโ€๐Ÿ’ป ืชื•ืจืžื™ ืงื•ื“ -
- -Code Contributors - -
- -## โญ๏ธ Stargazers -
- -Stargazers over time - -
- -## โš ๏ธ ืจื™ืฉื™ื•ืŸ -
- -
- -Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` is free and open-source software licensed under the [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Official logo was created by [Vic Shรณstak](https://github.com/koddr) and distributed under [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) license (CC BY-SA 4.0 International). -
- -
- -**ืจื™ืฉื™ื•ื ื•ืช ืฉืœ ืกืคืจื™ื•ืช ืฆื“ ืฉืœื™ืฉื™** -- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) -- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) -- [fasttemplate](https://github.com/valyala/fasttemplate/blob/master/LICENSE) -- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) -
diff --git a/.github/README_id.md b/.github/README_id.md deleted file mode 100644 index 2d9d4489..00000000 --- a/.github/README_id.md +++ /dev/null @@ -1,593 +0,0 @@ -

- - Fiber - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -

-

- Fiber adalah web framework yang terinspirasi dari Express yang berbasiskan Fasthttp, HTTP engine paling cepat untuk Go. Dirancang untuk mempermudah, mempercepat pengembangan aplikasi dengan alokasi memori nol-nya serta kinerja yang selalu diperhatikan. -

- -## โšก๏ธ Cara Mulai - -```go -package main - -import "github.com/gofiber/fiber" - -func main() { - app := fiber.New() - - app.Get("/", func(c *fiber.Ctx) { - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -## ๐Ÿค– Pengukuran Kinerja - -Pengukuran ini dilakukan oleh [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) dan [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Apabila anda ingin melihat hasil lengkapnya, silahkan kunjungi halaman [Wiki](https://docs.gofiber.io/benchmarks) kami. - -

- - -

- -## โš™๏ธ Instalasi - -Pertama, [unduh](https://golang.org/dl/) dan instal Go di komputer anda. Versi `1.11` atau yang lebih tinggi diperlukan. - -Instalasi dilakukkan dengan perintah [`go get`](https://golang.org/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them): - -```bash -go get -u github.com/gofiber/fiber/... -``` - -## ๐ŸŽฏ Fitur - -- Sistem [Routing](https://docs.gofiber.io/routing) yang solid -- Serve [file statis](https://docs.gofiber.io/application#static) -- [Kinerja](https://docs.gofiber.io/benchmarks) ekstrim -- [Penggunaan memori](https://docs.gofiber.io/benchmarks) yang kecil -- Cocok untuk [API](https://docs.gofiber.io/context) -- Mendukung Middleware & [Next](https://docs.gofiber.io/context#next) seperti Express -- Kembangkan aplikasi dengan [Cepat](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) -- [Template engines](https://github.com/gofiber/template) -- [WebSocket support](https://docs.gofiber.io/middleware#websocket) -- [Rate Limiter](https://docs.gofiber.io/middleware#limiter) -- Available in [15 languages](https://docs.gofiber.io/) -- Dan masih banyak lagi, [kunjungi Fiber](https://docs.gofiber.io/) - -## ๐Ÿ’ก Filosofi - -Bagi yang baru yang beralih dari [Node.js](https://nodejs.org/en/about/) ke [Go](https://golang.org/doc/) terkadang perlu waktu yang cukup lama sebelum mereka mampu membuat aplikasi web dengan Go. Fiber, sebagai **web framework** dirancang secara **minimalis** dan mengikuti filosofi dari **UNIX**, sehingga pengguna baru dengan cepat memasuki dunia Go dengan sambutan yang hangat dan dapat diandalkan. - -Fiber terinspirasi dari Express, salah satu web framework paling terkenal di Internet. Kami menggabungkan **kemudahan** dari Express dan **kinerja luar biasa** dari Go. Apabila anda pernah membuat aplikasi dengan Node.js (_dengan Express atau yang lainnya_), maka banyak metode dan prinsip yang akan terasa **sangat umum** bagi anda. - -Kami **mendengarkan** para pengguna di [GitHub Issues](https://github.com/gofiber/fiber/issues) (_dan berbagai platform lainnya_) untuk menciptakan web framework yang **cepat**, **fleksibel** dan **bersahabat** untuk berbagai macam keperluan, **tenggat waktu** dan **keahlian** para pengguna! Sama halnya seperti yang dilakukkan Express di dunia JavaScript. - -## ๐Ÿ‘€ Contoh - -Dibawah ini terdapat beberapa contoh penggunaan. Jika anda ingin contoh lainnya, silahkan kunjungi [Gudang resep](https://github.com/gofiber/recipes) atau kunjungi [Dokumentasi API](https://docs.gofiber.io) kami. - -Listed below are some of the common examples. If you want to see more code examples , please visit our [Recipes repository](https://github.com/gofiber/recipes) or visit our hosted [API documentation](https://docs.gofiber.io). - -#### ๐Ÿ“– [**Basic Routing**](https://docs.gofiber.io/#basic-routing) - -```go -func main() { - app := fiber.New() - - // GET /john - app.Get("/:name", func(c *fiber.Ctx) { - msg := fmt.Sprintf("Hello, %s ๐Ÿ‘‹!", c.Params("name")) - c.Send(msg) // => Hello john ๐Ÿ‘‹! - }) - - // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ‘ด %s is %s years old", c.Params("name"), c.Params("age")) - c.Send(msg) // => ๐Ÿ‘ด john is 75 years old - }) - - // GET /dictionary.txt - app.Get("/:file.:ext", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ“ƒ %s.%s", c.Params("file"), c.Params("ext")) - c.Send(msg) // => ๐Ÿ“ƒ dictionary.txt - }) - - // GET /flights/LAX-SFO - app.Get("/flights/:from-:to", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ’ธ From: %s, To: %s", c.Params("from"), c.Params("to")) - c.Send(msg) // => ๐Ÿ’ธ From: LAX, To: SFO - }) - - // GET /api/register - app.Get("/api/*", func(c *fiber.Ctx) { - msg := fmt.Sprintf("โœ‹ %s", c.Params("*")) - c.Send(msg) // => โœ‹ /api/register - }) - - app.Listen(3000) -} -``` - -#### ๐Ÿ“– [**Serving Static Files**](https://docs.gofiber.io/application#static) - -```go -func main() { - app := fiber.New() - - app.Static("/", "./public") - // => http://localhost:3000/js/script.js - // => http://localhost:3000/css/style.css - - app.Static("/prefix", "./public") - // => http://localhost:3000/prefix/js/script.js - // => http://localhost:3000/prefix/css/style.css - - app.Static("*", "./public/index.html") - // => http://localhost:3000/any/path/shows/index/html - - app.Listen(3000) -} -``` - -#### ๐Ÿ“– [**Middleware & Next**](https://docs.gofiber.io/context#next) - -```go -func main() { - app := fiber.New() - - // Match any route - app.Use(func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‡ First handler") - c.Next() - }) - - // Match all routes starting with /api - app.Use("/api", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅˆ Second handler") - c.Next() - }) - - // GET /api/register - app.Get("/api/list", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‰ Last handler") - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -
- ๐Ÿ“š Show more code examples - -### Views engines - -๐Ÿ“– [Settings](https://docs.gofiber.io/application#settings) -๐Ÿ“– [Engines](https://github.com/gofiber/template) -๐Ÿ“– [Render](https://docs.gofiber.io/context#render) - -Fiber defaults to the [html/template](https://golang.org/pkg/html/template/) when no view engine is set. - -If you want to execute partials or use a different engine like [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) or [pug](https://github.com/Joker/jade) etc.. - -Checkout our [Template](https://github.com/gofiber/template) package that support multiple view engines. - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/template/pug" -) - -func main() { - // You can setup Views engine before initiation app: - app := fiber.New(&fiber.Settings{ - Views: pug.New("./views", ".pug"), - }) - - // OR after initiation app at any convenient location: - app.Settings.Views = pug.New("./views", ".pug"), - - // And now, you can call template `./views/home.pug` like this: - app.Get("/", func(c *fiber.Ctx) { - c.Render("home", fiber.Map{ - "title": "Homepage", - "year": 1999, - }) - }) - - // ... -} -``` - -### Grouping routes into chains - -๐Ÿ“– [Group](https://docs.gofiber.io/application#group) - -```go -func main() { - app := fiber.New() - - // Root API route - api := app.Group("/api", cors()) // /api - - // API v1 routes - v1 := api.Group("/v1", mysql()) // /api/v1 - v1.Get("/list", handler) // /api/v1/list - v1.Get("/user", handler) // /api/v1/user - - // API v2 routes - v2 := api.Group("/v2", mongodb()) // /api/v2 - v2.Get("/list", handler) // /api/v2/list - v2.Get("/user", handler) // /api/v2/user - - // ... -} -``` - -### Middleware logger - -๐Ÿ“– [Logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - // Default - app.Use(middleware.Logger()) - - // Custom logging format - app.Use(middleware.Logger("${method} - ${path}")) - - // Custom Config - app.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ - Next: func(ctx *fiber.Ctx) bool { - return ctx.Path() != "/private" - }, - Format: "${method} - ${path}", - Output: io.Writer, - })) - - app.Listen(3000) -} -``` - -### Cross-Origin Resource Sharing (CORS) - -๐Ÿ“– [CORS](https://docs.gofiber.io/middleware#cors) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/cors" -) - -func main() { - app := fiber.New() - - // CORS with default config - app.Use(cors.New()) - - app.Listen(3000) -} -``` - -Check CORS by passing any domain in `Origin` header: - -```bash -curl -H "Origin: http://example.com" --verbose http://localhost:3000 -``` - -### Custom 404 response - -๐Ÿ“– [HTTP Methods](https://docs.gofiber.io/application#http-methods) - -```go -func main() { - app := fiber.New() - - app.Static("./public") - - app.Get("/demo", func(c *fiber.Ctx) { - c.Send("This is a demo!") - }) - - app.Post("/register", func(c *fiber.Ctx) { - c.Send("Welcome!") - }) - - // Last middleware to match anything - app.Use(func(c *fiber.Ctx) { - c.SendStatus(404) - // => 404 "Not Found" - }) - - app.Listen(3000) -} -``` - -### JSON Response - -๐Ÿ“– [JSON](https://docs.gofiber.io/context#json) - -```go -type User struct { - Name string `json:"name"` - Age int `json:"age"` -} - -func main() { - app := fiber.New() - - app.Get("/user", func(c *fiber.Ctx) { - c.JSON(&User{"John", 20}) - // => {"name":"John", "age":20} - }) - - app.Get("/json", func(c *fiber.Ctx) { - c.JSON(fiber.Map{ - "success": true, - "message": "Hi John!", - }) - // => {"success":true, "message":"Hi John!"} - }) - - app.Listen(3000) -} -``` - -### WebSocket Upgrade - -๐Ÿ“– [Websocket](https://docs.gofiber.io/middleware#websocket) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/websocket" -) - -func main() { - app := fiber.New() - - app.Get("/ws", websocket.New(func(c *websocket.Conn) { - for { - mt, msg, err := c.ReadMessage() - if err != nil { - log.Println("read:", err) - break - } - log.Printf("recv: %s", msg) - err = c.WriteMessage(mt, msg) - if err != nil { - log.Println("write:", err) - break - } - } - })) - - app.Listen(3000) - // ws://localhost:3000/ws -} -``` - -### Recover middleware - -๐Ÿ“– [Recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - app.Use(middleware.Recover()) - - app.Get("/", func(c *fiber.Ctx) { - panic("normally this would crash your app") - }) - - app.Listen(3000) -} -``` -
- -## ๐Ÿงฌ Fiber Middleware - -The Fiber middleware modules listed here are maintained by the [Fiber team](https://github.com/orgs/gofiber/people). - -| Middleware | Description | Built-in middleware | -| :--- | :--- | :--- | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | - | -| [basicauth](https://github.com/gofiber/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | - | -| [compress](https://github.com/gofiber/fiber/blob/master/middleware/compress.md) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | `middleware.Compress()` | -| [cors](https://github.com/gofiber/cors) | Enable cross-origin resource sharing \(CORS\) with various options. | - | -| [csrf](https://github.com/gofiber/csrf) | Protect from CSRF exploits. | - | -| [filesystem](https://github.com/gofiber/fiber/blob/master/middleware/filesystem.md) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | - | -| [favicon](https://github.com/gofiber/fiber/blob/master/middleware/favicon.md) | Ignore favicon from logs or serve from memory if a file path is provided. | `middleware.Favicon()` | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | - | -| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | - | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | - | -| [limiter](https://github.com/gofiber/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | - | -| [logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) | HTTP request/response logger. | `middleware.Logger()` | -| [pprof](https://github.com/gofiber/pprof) | Special thanks to Matthew Lee \(@mthli\) | - | -| [recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) | Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized[ ErrorHandler](error-handling.md). | `middleware.Recover()` | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | - | -| [requestid](https://github.com/gofiber/fiber/blob/master/middleware/request_id.md) | Request ID middleware generates a unique id for a request. | `middleware.RequestID()` | -| [session](https://github.com/gofiber/session) | This session middleware is build on top of fasthttp/session by @savsgio MIT. Special thanks to @thomasvvugt for helping with this middleware. | - | -| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | - | -| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | - | - -## ๐ŸŒฑ Third Party Middlewares - -This is a list of middlewares that are created by the Fiber community, please create a PR if you want to see yours! -- [arsmn/fiber-casbin](https://github.com/arsmn/fiber-casbin) -- [arsmn/fiber-introspect](https://github.com/arsmn/fiber-introspect) -- [arsmn/fiber-swagger](https://github.com/arsmn/fiber-swagger) -- [arsmn/gqlgen](https://github.com/arsmn/gqlgen) -- [codemicro/fiber-cache](https://github.com/codemicro/fiber-cache) -- [itsursujit/fiber-boilerplate](https://github.com/itsursujit/fiber-boilerplate) -- [juandiii/go-jwk-security](https://github.com/juandiii/go-jwk-security) -- [kiyonlin/fiber_limiter](https://github.com/kiyonlin/fiber_limiter) -- [shareed2k/fiber_limiter](https://github.com/shareed2k/fiber_limiter) -- [shareed2k/fiber_tracing](https://github.com/shareed2k/fiber_tracing) -- [thomasvvugt/fiber-boilerplate](https://github.com/thomasvvugt/fiber-boilerplate) - -## ๐Ÿ’ฌ Media - -

- - -

- -- [Welcome to Fiber โ€” an Express.js styled web framework written in Go with โค๏ธ](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) โ€” _03 Feb 2020_ -- [Fiber released v1.7! ๐ŸŽ‰ What's new and is it still fast, flexible and friendly?](https://dev.to/koddr/fiber-v2-is-out-now-what-s-new-and-is-he-still-fast-flexible-and-friendly-3ipf) โ€” _21 Feb 2020_ -- [๐Ÿš€ Fiber v1.8. What's new, updated and re-thinked?](https://dev.to/koddr/fiber-v1-8-what-s-new-updated-and-re-thinked-339h) โ€” _03 Mar 2020_ -- [Is switching from Express to Fiber worth it? ๐Ÿค”](https://dev.to/koddr/are-sure-what-your-lovely-web-framework-running-so-fast-2jl1) โ€” _01 Apr 2020_ -- [Creating Fast APIs In Go Using Fiber](https://dev.to/jozsefsallai/creating-fast-apis-in-go-using-fiber-59m9) โ€” _07 Apr 2020_ -- [Building a Basic REST API in Go using Fiber](https://tutorialedge.net/golang/basic-rest-api-go-fiber/) - _23 Apr 2020_ -- [๐Ÿ“บ Building a REST API using GORM and Fiber](https://youtu.be/Iq2qT0fRhAA) - _25 Apr 2020_ -- [๐ŸŒŽ Create a travel list app with Go, Fiber, Angular, MongoDB and Google Cloud Secret Manager](https://blog.yongweilun.me/create-a-travel-list-app-with-go-fiber-angular-mongodb-and-google-cloud-secret-manager-ck9fgxy0p061pcss1xt1ubu8t) - _25 Apr 2020_ -- [Fiber v1.9.6 ๐Ÿ”ฅ How to improve performance by 817% and stay fast, flexible and friendly?](https://dev.to/koddr/fiber-v1-9-5-how-to-improve-performance-by-817-and-stay-fast-flexible-and-friendly-2dp6) - _12 May 2020_ -- [The road to web-based authentication with Fiber โšก](https://vugt.me/the-road-to-web-based-authentication-with-fiber/) - _20 May 2020_ -- [Building an Express-style API in Go with Fiber](https://blog.logrocket.com/express-style-api-go-fiber/) - _10 June 2020_ -- [ๅŸบไบŽgolang fiberๅ’Œangularๅผ€ๅ‘web](https://zhuanlan.zhihu.com/p/148925642) - _19 June 2020_ -- [ๅŸบไบŽๅปถ่ฟŸ่ฎก็ฎ—ไปค็‰Œๆกถ็š„gofiber้ข‘็Ž‡้™ๅˆถไธญ้—ดไปถๅฎž็Žฐ](https://zhuanlan.zhihu.com/p/149308936) - _20 June 2020_ -- [Construir una API en Golang con Fiber ๐Ÿ‡ช๐Ÿ‡ธ](https://enbonnet.me/article/53/construir-api-golang-con-fiber) - _28 June 2020_ -- [๐Ÿ“บWhy Go Fiber Is THE New Framework To Learn](https://www.youtube.com/watch?v=kvwsPeWDLM8) - _29 June 2020_ -- [่งฃๆžGofiber่ทฏ็”ฑ็ฎก็†](https://zhuanlan.zhihu.com/p/152494502) - _08 July 2020_ -- [๐Ÿ“บ Introduction to Fiber - An Express-inspired web framework](https://youtu.be/MfFi4Gt-tos) - _25 July 2020_ - -## ๐Ÿ‘ Berkontribusi - -Apabila anda ingin mengucapkan **terima kasih** dan/atau mendukung pengembangan `Fiber`: - -1. Berikan bintang atau [GitHub Star](https://github.com/gofiber/fiber/stargazers) ke proyek ini. -2. Bagikan [di Twitter anda](https://twitter.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). -3. Buat ulasan atau tutorial di [Medium](https://medium.com/), [Dev.to](https://dev.to/) atau blog pribadi anda. -4. Help us to translate our API Documentation via [Crowdin](https://crowdin.com/project/gofiber) [![Crowdin](https://badges.crowdin.net/gofiber/localized.svg)](https://crowdin.com/project/gofiber) -5. Support the project by donating a [cup of coffee](https://buymeacoff.ee/fenny). - -## โ˜• Supporters - -Fiber is an open source project that runs on donations to pay the bills e.g. our domain name, gitbook, netlify and serverless hosting. If you want to support Fiber, you can โ˜• [**buy a coffee here**](https://buymeacoff.ee/fenny). - -| | User | Donation | -| :---------------------------------------------------------- | :----------------------------------------------- | :-------- | -| ![](https://avatars.githubusercontent.com/u/204341?s=25 ) | [@destari](https://github.com/destari) | โ˜• x 10 | -| ![](https://avatars.githubusercontent.com/u/63164982?s=25 ) | [@dembygenesis](https://github.com/dembygenesis) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/56607882?s=25 ) | [@thomasvvugt](https://github.com/thomasvvugt) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/27820675?s=25 ) | [@hendratommy](https://github.com/hendratommy) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/1094221?s=25 ) | [@ekaputra07](https://github.com/ekaputra07) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/194590?s=25 ) | [@jorgefuertes](https://github.com/jorgefuertes) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/186637?s=25 ) | [@candidosales](https://github.com/candidosales) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/29659953?s=25 ) | [@l0nax](https://github.com/l0nax) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/59947262?s=25 ) | [@ankush](https://github.com/ankush) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/635852?s=25 ) | [@bihe](https://github.com/bihe) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/307334?s=25 ) | [@justdave](https://github.com/justdave) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/11155743?s=25 ) | [@koddr](https://github.com/koddr) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/29042462?s=25 ) | [@lapolinar](https://github.com/lapolinar) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/2978730?s=25 ) | [@diegowifi](https://github.com/diegowifi) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/44171355?s=25 ) | [@ssimk0](https://github.com/ssimk0) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/5638101?s=25 ) | [@raymayemir](https://github.com/raymayemir) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/619996?s=25 ) | [@melkorm](https://github.com/melkorm) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31022056?s=25 ) | [@marvinjwendt](https://github.com/thomasvvugt) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31921460?s=25 ) | [@toishy](https://github.com/toishy) | โ˜• x 1 | - -## โ€Žโ€๐Ÿ’ป Code Contributors - -Code Contributors - -## โญ๏ธ Stargazers - -Stargazers over time - -## โš ๏ธ License - -Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` is free and open-source software licensed under the [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Official logo was created by [Vic Shรณstak](https://github.com/koddr) and distributed under [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) license (CC BY-SA 4.0 International). - -**Third-party library licenses** -- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) -- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) -- [fasttemplate](https://github.com/valyala/fasttemplate/blob/master/LICENSE) -- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) diff --git a/.github/README_ja.md b/.github/README_ja.md deleted file mode 100644 index 010506d7..00000000 --- a/.github/README_ja.md +++ /dev/null @@ -1,594 +0,0 @@ -

- - Fiber - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -

-

-FIberใฏใ€Expressใซ่งฆ็™บใ•ใ‚ŒใŸWebใƒ•ใƒฌใƒผใƒ ใƒฏใƒผใ‚ฏใงใ™ใ€‚Go ๆœ€้€ŸใฎHTTPใ‚จใƒณใ‚ธใƒณใงใ‚ใ‚‹Fasthttpใงไฝœใ‚‰ใ‚Œใฆใ„ใพใ™ใ€‚ใ‚ผใƒญใƒกใƒขใƒชใ‚ขใƒญใ‚ฑใƒผใ‚ทใƒงใƒณใจใƒ‘ใƒ•ใ‚ฉใƒผใƒžใƒณใ‚นใ‚’ๅฟต้ ญใซ็ฝฎใ„ใฆ่จญ่จˆใ•ใ‚ŒใฆใŠใ‚Šใ€่ฟ…้€Ÿใช้–‹็™บใ‚’ใ‚ตใƒใƒผใƒˆใ—ใพใ™ใ€‚ - -

- -## โšก๏ธ ใ‚ฏใ‚คใƒƒใ‚ฏใ‚นใ‚ฟใƒผใƒˆ - -```go -package main - -import "github.com/gofiber/fiber" - -func main() { - app := fiber.New() - - app.Get("/", func(c *fiber.Ctx) { - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` -## ๐Ÿค– ใƒ™ใƒณใƒใƒžใƒผใ‚ฏ - -ใ“ใ‚Œใ‚‰ใฎใƒ†ใ‚นใƒˆใฏ[TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext)ใŠใ‚ˆใณ[Go Web](https://github.com/smallnest/go-web-framework-benchmark)ใซใ‚ˆใฃใฆ่จˆๆธฌใ‚’่กŒใฃใฆใ„ใพใ™ ใ€‚ใ™ในใฆใฎ็ตๆžœใ‚’่กจ็คบใ™ใ‚‹ใซใฏใ€ [Wiki](https://docs.gofiber.io/benchmarks)ใซใ‚ขใ‚ฏใ‚ปใ‚นใ—ใฆใใ ใ•ใ„ใ€‚ - -

- - -

- -## โš™๏ธ ใ‚คใƒณใ‚นใƒˆใƒผใƒซ - -ใพใšใ€Goใ‚’[ใƒ€ใ‚ฆใƒณใƒญใƒผใƒ‰](https://golang.org/dl/)ใ—ใฆใใ ใ•ใ„ใ€‚ `1.11`ไปฅ้™ใŒๅฟ…่ฆใงใ™ใ€‚ - -ใใ—ใฆใ€[`go get`](https://golang.org/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them)ใ‚ณใƒžใƒณใƒ‰ใ‚’ไฝฟ็”จใ—ใฆใ‚คใƒณใ‚นใƒˆใƒผใƒซใ—ใฆใใ ใ•ใ„ใ€‚ - -```bash -go get -u github.com/gofiber/fiber/... -``` - -## ๐ŸŽฏ ๆฉŸ่ƒฝ - -- ๅ …็‰ขใช[ใƒซใƒผใƒ†ใ‚ฃใƒณใ‚ฐ](https://docs.gofiber.io/routing) -- [้™็š„ใƒ•ใ‚กใ‚คใƒซ](https://docs.gofiber.io/application#static)ใฎใ‚ตใƒใƒผใƒˆ -- ็ฉถๆฅตใฎ[ใƒ‘ใƒ•ใ‚ฉใƒผใƒžใƒณใ‚น](https://docs.gofiber.io/benchmarks) -- [ไฝŽใƒกใƒขใƒช](https://docs.gofiber.io/benchmarks)ใƒ•ใƒƒใƒˆใƒ—ใƒชใƒณใƒˆ -- Express [APIใ‚จใƒณใƒ‰ใƒใ‚คใƒณใƒˆ](https://docs.gofiber.io/context) -- Middlewareใจ[Next](https://docs.gofiber.io/context#next)ใฎใ‚ตใƒใƒผใƒˆ -- [่ฟ…้€Ÿ](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497)ใชใ‚ตใƒผใƒใƒผใ‚ตใ‚คใƒ‰ใƒ—ใƒญใ‚ฐใƒฉใƒŸใƒณใ‚ฐ -- [Template engines](https://github.com/gofiber/template) -- [WebSocket support](https://docs.gofiber.io/middleware#websocket) -- [Rate Limiter](https://docs.gofiber.io/middleware#limiter) -- Available in [15 languages](https://docs.gofiber.io/) -- [Fiber](https://docs.gofiber.io/)ใ‚’ใ‚‚ใฃใจ็Ÿฅใ‚‹ - -## ๐Ÿ’ก ๅ“ฒๅญฆ - -[Node.js](https://nodejs.org/en/about/)ใ‹ใ‚‰[Go](https://golang.org/doc/) ใซไน—ใ‚Šๆ›ใˆใ‚ˆใ†ใจใ—ใฆใ„ใ‚‹ๆ–ฐใ—ใ„GopherใฏWebใƒ•ใƒฌใƒผใƒ ใƒฏใƒผใ‚ฏใ‚„ใƒžใ‚คใ‚ฏใƒญใ‚ตใƒผใƒ“ใ‚นใฎๆง‹็ฏ‰ใ‚’ๅง‹ใ‚ใ‚‹ๅ‰ใซๅคšใใ‚’ๅญฆใฐใชใ‘ใ‚Œใฐใชใ‚Šใพใ›ใ‚“ใ€‚ -ใ—ใ‹ใ—ใ€ **Webใƒ•ใƒฌใƒผใƒ ใƒฏใƒผใ‚ฏ**ใงใ‚ใ‚‹Fiberใฏ**ใƒŸใƒ‹ใƒžใƒชใ‚บใƒ **ใจ**UNIXๅ“ฒๅญฆ**ใ‚’ใ‚‚ใจใซไฝœใ‚‰ใ‚Œใฆใ„ใ‚‹ใŸใ‚ใ€ๆ–ฐใ—ใ„Gopherใฏใ‚นใƒ ใƒผใ‚บใซGoใฎไธ–็•Œใซๅ…ฅใ‚‹ใ“ใจใŒใงใใพใ™ใ€‚ - -Fiberใฏไบบๆฐ—ใฎ้ซ˜ใ„Webใƒ•ใƒฌใƒผใƒ ใƒฏใƒผใ‚ฏใงใ‚ใ‚‹Expressjsใซ**ใ‚คใƒณใ‚นใƒ‘ใ‚คใ‚ข**ใ•ใ‚Œใฆใ„ใพใ™ใ€‚ -ใ‚ใŸใ—ใŸใกใฏ Expressใฎ**ๆ‰‹่ปฝใ•**ใจGoใฎ**ใƒ‘ใƒ•ใ‚ฉใƒผใƒžใƒณใ‚น**ใ‚’็ต„ใฟๅˆใ‚ใ›ใพใ—ใŸใ€‚ -ใ‚‚ใ—ใ‚‚ใ€Webใ‚ขใƒ—ใƒชใ‚ฑใƒผใ‚ทใƒงใƒณใ‚’Express็ญ‰ใฎNode.jsใƒ•ใƒฌใƒผใƒ ใƒฏใƒผใ‚ฏใงๅฎŸ่ฃ…ใ—ใŸ็ตŒ้จ“ใŒใ‚ใ‚Œใฐใ€ๅคšใใฎๆ–นๆณ•ใ‚„ๅŽŸ็†ใŒใจใฆใ‚‚**้ฆดๆŸ“ใฟๆทฑใ„**ใงใ—ใ‚‡ใ†ใ€‚ - -## ๐Ÿ‘€ ไพ‹ - -ไปฅไธ‹ใซไธ€่ˆฌ็š„ใชไพ‹ใ‚’ใ„ใใคใ‹็คบใ—ใพใ™ใ€‚ไป–ใฎใ‚ณใƒผใƒ‰ไพ‹ใ‚’ใ”่ฆงใซใชใ‚ŠใŸใ„ๅ ดๅˆใฏใ€ [Recipesใƒชใƒใ‚ธใƒˆใƒช](https://github.com/gofiber/recipes)ใพใŸใฏ[APIใƒ‰ใ‚ญใƒฅใƒกใƒณใƒˆ](https://docs.gofiber.io)ใซใ‚ขใ‚ฏใ‚ปใ‚นใ—ใฆใใ ใ•ใ„ใ€‚ - -Listed below are some of the common examples. If you want to see more code examples , please visit our [Recipes repository](https://github.com/gofiber/recipes) or visit our hosted [API documentation](https://docs.gofiber.io). - -#### ๐Ÿ“– [**Basic Routing**](https://docs.gofiber.io/#basic-routing) - -```go -func main() { - app := fiber.New() - - // GET /john - app.Get("/:name", func(c *fiber.Ctx) { - msg := fmt.Sprintf("Hello, %s ๐Ÿ‘‹!", c.Params("name")) - c.Send(msg) // => Hello john ๐Ÿ‘‹! - }) - - // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ‘ด %s is %s years old", c.Params("name"), c.Params("age")) - c.Send(msg) // => ๐Ÿ‘ด john is 75 years old - }) - - // GET /dictionary.txt - app.Get("/:file.:ext", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ“ƒ %s.%s", c.Params("file"), c.Params("ext")) - c.Send(msg) // => ๐Ÿ“ƒ dictionary.txt - }) - - // GET /flights/LAX-SFO - app.Get("/flights/:from-:to", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ’ธ From: %s, To: %s", c.Params("from"), c.Params("to")) - c.Send(msg) // => ๐Ÿ’ธ From: LAX, To: SFO - }) - - // GET /api/register - app.Get("/api/*", func(c *fiber.Ctx) { - msg := fmt.Sprintf("โœ‹ %s", c.Params("*")) - c.Send(msg) // => โœ‹ /api/register - }) - - app.Listen(3000) -} -``` - -#### ๐Ÿ“– [**Serving Static Files**](https://docs.gofiber.io/application#static) - -```go -func main() { - app := fiber.New() - - app.Static("/", "./public") - // => http://localhost:3000/js/script.js - // => http://localhost:3000/css/style.css - - app.Static("/prefix", "./public") - // => http://localhost:3000/prefix/js/script.js - // => http://localhost:3000/prefix/css/style.css - - app.Static("*", "./public/index.html") - // => http://localhost:3000/any/path/shows/index/html - - app.Listen(3000) -} -``` - -#### ๐Ÿ“– [**Middleware & Next**](https://docs.gofiber.io/context#next) - -```go -func main() { - app := fiber.New() - - // Match any route - app.Use(func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‡ First handler") - c.Next() - }) - - // Match all routes starting with /api - app.Use("/api", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅˆ Second handler") - c.Next() - }) - - // GET /api/register - app.Get("/api/list", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‰ Last handler") - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -
- ๐Ÿ“š Show more code examples - -### Views engines - -๐Ÿ“– [Settings](https://docs.gofiber.io/application#settings) -๐Ÿ“– [Engines](https://github.com/gofiber/template) -๐Ÿ“– [Render](https://docs.gofiber.io/context#render) - -Fiber defaults to the [html/template](https://golang.org/pkg/html/template/) when no view engine is set. - -If you want to execute partials or use a different engine like [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) or [pug](https://github.com/Joker/jade) etc.. - -Checkout our [Template](https://github.com/gofiber/template) package that support multiple view engines. - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/template/pug" -) - -func main() { - // You can setup Views engine before initiation app: - app := fiber.New(&fiber.Settings{ - Views: pug.New("./views", ".pug"), - }) - - // OR after initiation app at any convenient location: - app.Settings.Views = pug.New("./views", ".pug"), - - // And now, you can call template `./views/home.pug` like this: - app.Get("/", func(c *fiber.Ctx) { - c.Render("home", fiber.Map{ - "title": "Homepage", - "year": 1999, - }) - }) - - // ... -} -``` - -### Grouping routes into chains - -๐Ÿ“– [Group](https://docs.gofiber.io/application#group) - -```go -func main() { - app := fiber.New() - - // Root API route - api := app.Group("/api", cors()) // /api - - // API v1 routes - v1 := api.Group("/v1", mysql()) // /api/v1 - v1.Get("/list", handler) // /api/v1/list - v1.Get("/user", handler) // /api/v1/user - - // API v2 routes - v2 := api.Group("/v2", mongodb()) // /api/v2 - v2.Get("/list", handler) // /api/v2/list - v2.Get("/user", handler) // /api/v2/user - - // ... -} -``` - -### Middleware logger - -๐Ÿ“– [Logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - // Default - app.Use(middleware.Logger()) - - // Custom logging format - app.Use(middleware.Logger("${method} - ${path}")) - - // Custom Config - app.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ - Next: func(ctx *fiber.Ctx) bool { - return ctx.Path() != "/private" - }, - Format: "${method} - ${path}", - Output: io.Writer, - })) - - app.Listen(3000) -} -``` - -### Cross-Origin Resource Sharing (CORS) - -๐Ÿ“– [CORS](https://docs.gofiber.io/middleware#cors) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/cors" -) - -func main() { - app := fiber.New() - - // CORS with default config - app.Use(cors.New()) - - app.Listen(3000) -} -``` - -Check CORS by passing any domain in `Origin` header: - -```bash -curl -H "Origin: http://example.com" --verbose http://localhost:3000 -``` - -### Custom 404 response - -๐Ÿ“– [HTTP Methods](https://docs.gofiber.io/application#http-methods) - -```go -func main() { - app := fiber.New() - - app.Static("./public") - - app.Get("/demo", func(c *fiber.Ctx) { - c.Send("This is a demo!") - }) - - app.Post("/register", func(c *fiber.Ctx) { - c.Send("Welcome!") - }) - - // Last middleware to match anything - app.Use(func(c *fiber.Ctx) { - c.SendStatus(404) - // => 404 "Not Found" - }) - - app.Listen(3000) -} -``` - -### JSON Response - -๐Ÿ“– [JSON](https://docs.gofiber.io/context#json) - -```go -type User struct { - Name string `json:"name"` - Age int `json:"age"` -} - -func main() { - app := fiber.New() - - app.Get("/user", func(c *fiber.Ctx) { - c.JSON(&User{"John", 20}) - // => {"name":"John", "age":20} - }) - - app.Get("/json", func(c *fiber.Ctx) { - c.JSON(fiber.Map{ - "success": true, - "message": "Hi John!", - }) - // => {"success":true, "message":"Hi John!"} - }) - - app.Listen(3000) -} -``` - -### WebSocket Upgrade - -๐Ÿ“– [Websocket](https://docs.gofiber.io/middleware#websocket) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/websocket" -) - -func main() { - app := fiber.New() - - app.Get("/ws", websocket.New(func(c *websocket.Conn) { - for { - mt, msg, err := c.ReadMessage() - if err != nil { - log.Println("read:", err) - break - } - log.Printf("recv: %s", msg) - err = c.WriteMessage(mt, msg) - if err != nil { - log.Println("write:", err) - break - } - } - })) - - app.Listen(3000) - // ws://localhost:3000/ws -} -``` - -### Recover middleware - -๐Ÿ“– [Recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - app.Use(middleware.Recover()) - - app.Get("/", func(c *fiber.Ctx) { - panic("normally this would crash your app") - }) - - app.Listen(3000) -} -``` -
- -## ๐Ÿงฌ Fiber Middleware - -The Fiber middleware modules listed here are maintained by the [Fiber team](https://github.com/orgs/gofiber/people). - -| Middleware | Description | Built-in middleware | -| :--- | :--- | :--- | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | - | -| [basicauth](https://github.com/gofiber/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | - | -| [compress](https://github.com/gofiber/fiber/blob/master/middleware/compress.md) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | `middleware.Compress()` | -| [cors](https://github.com/gofiber/cors) | Enable cross-origin resource sharing \(CORS\) with various options. | - | -| [csrf](https://github.com/gofiber/csrf) | Protect from CSRF exploits. | - | -| [filesystem](https://github.com/gofiber/fiber/blob/master/middleware/filesystem.md) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | - | -| [favicon](https://github.com/gofiber/fiber/blob/master/middleware/favicon.md) | Ignore favicon from logs or serve from memory if a file path is provided. | `middleware.Favicon()` | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | - | -| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | - | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | - | -| [limiter](https://github.com/gofiber/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | - | -| [logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) | HTTP request/response logger. | `middleware.Logger()` | -| [pprof](https://github.com/gofiber/pprof) | Special thanks to Matthew Lee \(@mthli\) | - | -| [recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) | Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized[ ErrorHandler](error-handling.md). | `middleware.Recover()` | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | - | -| [requestid](https://github.com/gofiber/fiber/blob/master/middleware/request_id.md) | Request ID middleware generates a unique id for a request. | `middleware.RequestID()` | -| [session](https://github.com/gofiber/session) | This session middleware is build on top of fasthttp/session by @savsgio MIT. Special thanks to @thomasvvugt for helping with this middleware. | - | -| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | - | -| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | - | - -## ๐ŸŒฑ Third Party Middlewares - -This is a list of middlewares that are created by the Fiber community, please create a PR if you want to see yours! -- [arsmn/fiber-casbin](https://github.com/arsmn/fiber-casbin) -- [arsmn/fiber-introspect](https://github.com/arsmn/fiber-introspect) -- [arsmn/fiber-swagger](https://github.com/arsmn/fiber-swagger) -- [arsmn/gqlgen](https://github.com/arsmn/gqlgen) -- [codemicro/fiber-cache](https://github.com/codemicro/fiber-cache) -- [itsursujit/fiber-boilerplate](https://github.com/itsursujit/fiber-boilerplate) -- [juandiii/go-jwk-security](https://github.com/juandiii/go-jwk-security) -- [kiyonlin/fiber_limiter](https://github.com/kiyonlin/fiber_limiter) -- [shareed2k/fiber_limiter](https://github.com/shareed2k/fiber_limiter) -- [shareed2k/fiber_tracing](https://github.com/shareed2k/fiber_tracing) -- [thomasvvugt/fiber-boilerplate](https://github.com/thomasvvugt/fiber-boilerplate) - -## ๐Ÿ’ฌ ใƒกใƒ‡ใ‚ฃใ‚ข - -

- - -

- -- [Welcome to Fiber โ€” an Express.js styled web framework written in Go with โค๏ธ](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) โ€” _03 Feb 2020_ -- [Fiber released v1.7! ๐ŸŽ‰ What's new and is it still fast, flexible and friendly?](https://dev.to/koddr/fiber-v2-is-out-now-what-s-new-and-is-he-still-fast-flexible-and-friendly-3ipf) โ€” _21 Feb 2020_ -- [๐Ÿš€ Fiber v1.8. What's new, updated and re-thinked?](https://dev.to/koddr/fiber-v1-8-what-s-new-updated-and-re-thinked-339h) โ€” _03 Mar 2020_ -- [Is switching from Express to Fiber worth it? ๐Ÿค”](https://dev.to/koddr/are-sure-what-your-lovely-web-framework-running-so-fast-2jl1) โ€” _01 Apr 2020_ -- [Creating Fast APIs In Go Using Fiber](https://dev.to/jozsefsallai/creating-fast-apis-in-go-using-fiber-59m9) โ€” _07 Apr 2020_ -- [Building a Basic REST API in Go using Fiber](https://tutorialedge.net/golang/basic-rest-api-go-fiber/) - _23 Apr 2020_ -- [๐Ÿ“บ Building a REST API using GORM and Fiber](https://youtu.be/Iq2qT0fRhAA) - _25 Apr 2020_ -- [๐ŸŒŽ Create a travel list app with Go, Fiber, Angular, MongoDB and Google Cloud Secret Manager](https://blog.yongweilun.me/create-a-travel-list-app-with-go-fiber-angular-mongodb-and-google-cloud-secret-manager-ck9fgxy0p061pcss1xt1ubu8t) - _25 Apr 2020_ -- [Fiber v1.9.6 ๐Ÿ”ฅ How to improve performance by 817% and stay fast, flexible and friendly?](https://dev.to/koddr/fiber-v1-9-5-how-to-improve-performance-by-817-and-stay-fast-flexible-and-friendly-2dp6) - _12 May 2020_ -- [The road to web-based authentication with Fiber โšก](https://vugt.me/the-road-to-web-based-authentication-with-fiber/) - _20 May 2020_ -- [Building an Express-style API in Go with Fiber](https://blog.logrocket.com/express-style-api-go-fiber/) - _10 June 2020_ -- [ๅŸบไบŽgolang fiberๅ’Œangularๅผ€ๅ‘web](https://zhuanlan.zhihu.com/p/148925642) - _19 June 2020_ -- [ๅŸบไบŽๅปถ่ฟŸ่ฎก็ฎ—ไปค็‰Œๆกถ็š„gofiber้ข‘็Ž‡้™ๅˆถไธญ้—ดไปถๅฎž็Žฐ](https://zhuanlan.zhihu.com/p/149308936) - _20 June 2020_ -- [Construir una API en Golang con Fiber ๐Ÿ‡ช๐Ÿ‡ธ](https://enbonnet.me/article/53/construir-api-golang-con-fiber) - _28 June 2020_ -- [๐Ÿ“บWhy Go Fiber Is THE New Framework To Learn](https://www.youtube.com/watch?v=kvwsPeWDLM8) - _29 June 2020_ -- [่งฃๆžGofiber่ทฏ็”ฑ็ฎก็†](https://zhuanlan.zhihu.com/p/152494502) - _08 July 2020_ -- [๐Ÿ“บ Introduction to Fiber - An Express-inspired web framework](https://youtu.be/MfFi4Gt-tos) - _25 July 2020_ - -## ๐Ÿ‘ ่ฒข็Œฎใ™ใ‚‹ - -`Fiber`ใซ้–‹็™บๆ”ฏๆดใ—ใฆใใ ใ•ใ‚‹ใชใ‚‰: - -1. [GitHub Star](https://github.com/gofiber/fiber/stargazers)ใ‚’ใคใ‘ใฆใใ ใ•ใ„ ใ€‚ -2. [ใ‚ใชใŸใฎTwitterใง](https://twitter.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber)ใƒ—ใƒญใ‚ธใ‚งใ‚ฏใƒˆใซใคใ„ใฆใƒ„ใ‚คใƒผใƒˆใ—ใฆใใ ใ•ใ„ใ€‚ -3. [Medium](https://medium.com/) ใ€ [Dev.toใ€](https://dev.to/)ใพใŸใฏๅ€‹ไบบใฎใƒ–ใƒญใ‚ฐใงใƒฌใƒ“ใƒฅใƒผใพใŸใฏใƒใƒฅใƒผใƒˆใƒชใ‚ขใƒซใ‚’ๆ›ธใ„ใฆใใ ใ•ใ„ใ€‚ -4. Help us to translate our API Documentation via [Crowdin](https://crowdin.com/project/gofiber) [![Crowdin](https://badges.crowdin.net/gofiber/localized.svg)](https://crowdin.com/project/gofiber) -5. Support the project by donating a [cup of coffee](https://buymeacoff.ee/fenny). - -## โ˜• Supporters - -Fiber is an open source project that runs on donations to pay the bills e.g. our domain name, gitbook, netlify and serverless hosting. If you want to support Fiber, you can โ˜• [**buy a coffee here**](https://buymeacoff.ee/fenny). - -| | User | Donation | -| :---------------------------------------------------------- | :----------------------------------------------- | :-------- | -| ![](https://avatars.githubusercontent.com/u/204341?s=25 ) | [@destari](https://github.com/destari) | โ˜• x 10 | -| ![](https://avatars.githubusercontent.com/u/63164982?s=25 ) | [@dembygenesis](https://github.com/dembygenesis) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/56607882?s=25 ) | [@thomasvvugt](https://github.com/thomasvvugt) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/27820675?s=25 ) | [@hendratommy](https://github.com/hendratommy) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/1094221?s=25 ) | [@ekaputra07](https://github.com/ekaputra07) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/194590?s=25 ) | [@jorgefuertes](https://github.com/jorgefuertes) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/186637?s=25 ) | [@candidosales](https://github.com/candidosales) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/29659953?s=25 ) | [@l0nax](https://github.com/l0nax) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/59947262?s=25 ) | [@ankush](https://github.com/ankush) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/635852?s=25 ) | [@bihe](https://github.com/bihe) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/307334?s=25 ) | [@justdave](https://github.com/justdave) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/11155743?s=25 ) | [@koddr](https://github.com/koddr) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/29042462?s=25 ) | [@lapolinar](https://github.com/lapolinar) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/2978730?s=25 ) | [@diegowifi](https://github.com/diegowifi) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/44171355?s=25 ) | [@ssimk0](https://github.com/ssimk0) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/5638101?s=25 ) | [@raymayemir](https://github.com/raymayemir) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/619996?s=25 ) | [@melkorm](https://github.com/melkorm) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31022056?s=25 ) | [@marvinjwendt](https://github.com/thomasvvugt) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31921460?s=25 ) | [@toishy](https://github.com/toishy) | โ˜• x 1 | - -## โ€Žโ€๐Ÿ’ป Code Contributors - -Code Contributors - -## โญ๏ธ Stargazers - -Stargazers over time - -## โš ๏ธ License - -Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` is free and open-source software licensed under the [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Official logo was created by [Vic Shรณstak](https://github.com/koddr) and distributed under [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) license (CC BY-SA 4.0 International). - -**Third-party library licenses** -- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) -- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) -- [fasttemplate](https://github.com/valyala/fasttemplate/blob/master/LICENSE) -- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) diff --git a/.github/README_ko.md b/.github/README_ko.md deleted file mode 100644 index 93238014..00000000 --- a/.github/README_ko.md +++ /dev/null @@ -1,596 +0,0 @@ -

- - Fiber - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -

-

- Fiber๋Š” Express์—์„œ ์˜๊ฐ์„ ๋ฐ›๊ณ , Go๋ฅผ ์œ„ํ•œ ๊ฐ€์žฅ ๋น ๋ฅธ HTTP ์—”์ง„์ธ Fasthttp๋ฅผ ํ† ๋Œ€๋กœ ๋งŒ๋“ค์–ด์ง„ ์›น ํ”„๋ ˆ์ž„์›Œํฌ ์ž…๋‹ˆ๋‹ค. ๋น„ ๋ฉ”๋ชจ๋ฆฌ ํ• ๋‹น๊ณผ ์„ฑ๋Šฅ์„ ๊ณ ๋ คํ•œ ๋น ๋ฅธ ๊ฐœ๋ฐœ์„ ์œ„ํ•ด ์†์‰ฝ๊ฒŒ ์‚ฌ์šฉ๋˜๋„๋ก ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. -

- -## โšก๏ธ ๋น ๋ฅธ ์‹œ์ž‘ - -```go -package main - -import "github.com/gofiber/fiber" - -func main() { - app := fiber.New() - - app.Get("/", func(c *fiber.Ctx) { - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -## ๐Ÿค– ๋ฒค์น˜๋งˆํฌ - -์ด ํ…Œ์ŠคํŠธ๋“ค์€ [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext)์™€ [Go Web](https://github.com/smallnest/go-web-framework-benchmark)์„ ํ†ตํ•ด ์ธก์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋งŒ์•ฝ ๋ชจ๋“  ๊ฒฐ๊ณผ๋ฅผ ๋ณด๊ณ  ์‹ถ๋‹ค๋ฉด, [Wiki](https://docs.gofiber.io/benchmarks)๋ฅผ ํ™•์ธํ•ด ์ฃผ์„ธ์š”. - -

- - -

- - -## โš™๏ธ ์„ค์น˜ - -์šฐ์„ , Go๋ฅผ [๋‹ค์šด๋กœ๋“œ](https://golang.org/dl/)ํ•˜๊ณ  ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค. `1.11` ๋ฒ„์ „ ์ด์ƒ์ด ์š”๊ตฌ๋ฉ๋‹ˆ๋‹ค. - -[`go get`](https://golang.org/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) ๋ช…๋ น์–ด๋ฅผ ์ด์šฉํ•ด ์„ค์น˜๊ฐ€ ์™„๋ฃŒ๋ฉ๋‹ˆ๋‹ค: - -```bash -go get -u github.com/gofiber/fiber/... -``` - -## ๐ŸŽฏ ํŠน์ง• - -- ๊ฒฌ๊ณ ํ•œ [๋ผ์šฐํŒ…](https://docs.gofiber.io/routing) -- [์ •์  ํŒŒ์ผ](https://docs.gofiber.io/application#static) ์ œ๊ณต -- ๋›ฐ์–ด๋‚œ [์„ฑ๋Šฅ](https://docs.gofiber.io/benchmarks) -- [์ ์€ ๋ฉ”๋ชจ๋ฆฌ](https://docs.gofiber.io/benchmarks) ๊ณต๊ฐ„ -- [API ์—”๋“œํฌ์ธํŠธ](https://docs.gofiber.io/context) -- ๋ฏธ๋“ค์›จ์–ด & [Next](https://docs.gofiber.io/context#next) ์ง€์› -- [๋น ๋ฅธ](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ํ”„๋กœ๊ทธ๋ž˜๋ฐ -- [Template engines](https://github.com/gofiber/template) -- [WebSocket support](https://docs.gofiber.io/middleware#websocket) -- [Rate Limiter](https://docs.gofiber.io/middleware#limiter) -- Available in [15 languages](https://docs.gofiber.io/) -- ๋” ์•Œ๊ณ  ์‹ถ๋‹ค๋ฉด, [Fiber ๋‘˜๋Ÿฌ๋ณด๊ธฐ](https://docs.gofiber.io/) - -## ๐Ÿ’ก ์ฒ ํ•™ - -[Node.js](https://nodejs.org/en/about/)์—์„œ [Go](https://golang.org/doc/)๋กœ ์ „ํ™˜ํ•˜๋Š” ์ƒˆ๋กœ์šด ๊ณ ํผ๋ถ„๋“ค์€ ์›น ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์ด๋‚˜ ๋งˆ์ดํฌ๋กœ ์„œ๋น„์Šค ๊ฐœ๋ฐœ์„ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜๊ธฐ ์ „์— ํ•™์Šต ๊ณก์„ ์— ์‹œ๋‹ฌ๋ฆฌ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. Fiber๋Š” **์›น ํ”„๋ ˆ์ž„์›Œํฌ**๋กœ์„œ, ์ƒˆ๋กœ์šด ๊ณ ํผ๋ถ„๋“ค์ด ๋”ฐ๋œปํ•˜๊ณ  ๋ฏฟ์Œ์งํ•œ ํ™˜์˜์„ ๊ฐ€์ง€๊ณ  ๋น ๋ฅด๊ฒŒ Go์˜ ์„ธ์ƒ์— ์ง„์ž…ํ•  ์ˆ˜ ์žˆ๊ฒŒ **๋ฏธ๋‹ˆ๋ฉ€๋ฆฌ์ฆ˜**์˜ ๊ฐœ๋…๊ณผ **UNIX ๋ฐฉ์‹**์— ๋”ฐ๋ผ ๊ฐœ๋ฐœ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. - -Fiber๋Š” ์ธํ„ฐ๋„ท์—์„œ ๊ฐ€์žฅ ์ธ๊ธฐ์žˆ๋Š” ์›น ํ”„๋ ˆ์ž„์›Œํฌ์ธ Express์—์„œ **์˜๊ฐ์„ ๋ฐ›์•˜์Šต๋‹ˆ๋‹ค.** ์šฐ๋ฆฌ๋Š” Express์˜ **์‰ฌ์šด** ์‚ฌ์šฉ๊ณผ Go์˜ **์„ฑ๋Šฅ**์„ ๊ฒฐํ•ฉํ•˜์˜€์Šต๋‹ˆ๋‹ค. ๋งŒ์•ฝ ๋‹น์‹ ์ด Node.js (Express ๋˜๋Š” ๋น„์Šทํ•œ ๊ฒƒ์„ ์‚ฌ์šฉํ•˜์—ฌ) ๋กœ ์›น ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ฐœ๋ฐœํ•œ ๊ฒฝํ—˜์ด ์žˆ๋‹ค๋ฉด, ๋งŽ์€ ๋ฉ”์†Œ๋“œ๋“ค๊ณผ ์›๋ฆฌ๋“ค์ด **๋งค์šฐ ๋น„์Šทํ•˜๊ฒŒ** ๋Š๊ปด์งˆ ๊ฒƒ ์ž…๋‹ˆ๋‹ค. - -์šฐ๋ฆฌ๋Š” **์–ด๋–คํ•œ** ์ž‘์—…, **๋งˆ๊ฐ์ผ์ •**, ๊ฐœ๋ฐœ์ž์˜ **๊ธฐ์ˆ **์ด๋˜๊ฐ„์— **๋น ๋ฅด๊ณ **, **์œ ์—ฐํ•˜๊ณ **, **์ต์ˆ™ํ•œ** Go ์›น ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ์ž๋“ค์˜ [์ด์Šˆ๋“ค](https://github.com/gofiber/fiber/issues)์„(๊ทธ๋ฆฌ๊ณ  ๋ชจ๋“  ์ธํ„ฐ๋„ท์„ ํ†ตํ•ด) **๋“ฃ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค**! Express๊ฐ€ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์„ธ๊ณ„์—์„œ ํ•˜๋Š” ๊ฒƒ ์ฒ˜๋Ÿผ์š”. - -## ๐Ÿ‘€ ์˜ˆ์ œ - -๋‹ค์Œ์€ ์ผ๋ฐ˜์ ์ธ ์˜ˆ์ œ๋“ค ์ž…๋‹ˆ๋‹ค. - -> ๋” ๋งŽ์€ ์ฝ”๋“œ ์˜ˆ์ œ๋ฅผ ๋ณด๊ณ  ์‹ถ๋‹ค๋ฉด, [Recipes ์ €์žฅ์†Œ](https://github.com/gofiber/recipes) ๋˜๋Š” [API ๋ฌธ์„œ](https://docs.gofiber.io)๋ฅผ ๋ฐฉ๋ฌธํ•˜์„ธ์š”. - -Listed below are some of the common examples. If you want to see more code examples , please visit our [Recipes repository](https://github.com/gofiber/recipes) or visit our hosted [API documentation](https://docs.gofiber.io). - -#### ๐Ÿ“– [**Basic Routing**](https://docs.gofiber.io/#basic-routing) - -```go -func main() { - app := fiber.New() - - // GET /john - app.Get("/:name", func(c *fiber.Ctx) { - msg := fmt.Sprintf("Hello, %s ๐Ÿ‘‹!", c.Params("name")) - c.Send(msg) // => Hello john ๐Ÿ‘‹! - }) - - // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ‘ด %s is %s years old", c.Params("name"), c.Params("age")) - c.Send(msg) // => ๐Ÿ‘ด john is 75 years old - }) - - // GET /dictionary.txt - app.Get("/:file.:ext", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ“ƒ %s.%s", c.Params("file"), c.Params("ext")) - c.Send(msg) // => ๐Ÿ“ƒ dictionary.txt - }) - - // GET /flights/LAX-SFO - app.Get("/flights/:from-:to", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ’ธ From: %s, To: %s", c.Params("from"), c.Params("to")) - c.Send(msg) // => ๐Ÿ’ธ From: LAX, To: SFO - }) - - // GET /api/register - app.Get("/api/*", func(c *fiber.Ctx) { - msg := fmt.Sprintf("โœ‹ %s", c.Params("*")) - c.Send(msg) // => โœ‹ /api/register - }) - - app.Listen(3000) -} -``` - -#### ๐Ÿ“– [**Serving Static Files**](https://docs.gofiber.io/application#static) - -```go -func main() { - app := fiber.New() - - app.Static("/", "./public") - // => http://localhost:3000/js/script.js - // => http://localhost:3000/css/style.css - - app.Static("/prefix", "./public") - // => http://localhost:3000/prefix/js/script.js - // => http://localhost:3000/prefix/css/style.css - - app.Static("*", "./public/index.html") - // => http://localhost:3000/any/path/shows/index/html - - app.Listen(3000) -} -``` - -#### ๐Ÿ“– [**Middleware & Next**](https://docs.gofiber.io/context#next) - -```go -func main() { - app := fiber.New() - - // Match any route - app.Use(func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‡ First handler") - c.Next() - }) - - // Match all routes starting with /api - app.Use("/api", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅˆ Second handler") - c.Next() - }) - - // GET /api/register - app.Get("/api/list", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‰ Last handler") - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -
- ๐Ÿ“š Show more code examples - -### Views engines - -๐Ÿ“– [Settings](https://docs.gofiber.io/application#settings) -๐Ÿ“– [Engines](https://github.com/gofiber/template) -๐Ÿ“– [Render](https://docs.gofiber.io/context#render) - -Fiber defaults to the [html/template](https://golang.org/pkg/html/template/) when no view engine is set. - -If you want to execute partials or use a different engine like [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) or [pug](https://github.com/Joker/jade) etc.. - -Checkout our [Template](https://github.com/gofiber/template) package that support multiple view engines. - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/template/pug" -) - -func main() { - // You can setup Views engine before initiation app: - app := fiber.New(&fiber.Settings{ - Views: pug.New("./views", ".pug"), - }) - - // OR after initiation app at any convenient location: - app.Settings.Views = pug.New("./views", ".pug"), - - // And now, you can call template `./views/home.pug` like this: - app.Get("/", func(c *fiber.Ctx) { - c.Render("home", fiber.Map{ - "title": "Homepage", - "year": 1999, - }) - }) - - // ... -} -``` - -### Grouping routes into chains - -๐Ÿ“– [Group](https://docs.gofiber.io/application#group) - -```go -func main() { - app := fiber.New() - - // Root API route - api := app.Group("/api", cors()) // /api - - // API v1 routes - v1 := api.Group("/v1", mysql()) // /api/v1 - v1.Get("/list", handler) // /api/v1/list - v1.Get("/user", handler) // /api/v1/user - - // API v2 routes - v2 := api.Group("/v2", mongodb()) // /api/v2 - v2.Get("/list", handler) // /api/v2/list - v2.Get("/user", handler) // /api/v2/user - - // ... -} -``` - -### Middleware logger - -๐Ÿ“– [Logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - // Default - app.Use(middleware.Logger()) - - // Custom logging format - app.Use(middleware.Logger("${method} - ${path}")) - - // Custom Config - app.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ - Next: func(ctx *fiber.Ctx) bool { - return ctx.Path() != "/private" - }, - Format: "${method} - ${path}", - Output: io.Writer, - })) - - app.Listen(3000) -} -``` - -### Cross-Origin Resource Sharing (CORS) - -๐Ÿ“– [CORS](https://docs.gofiber.io/middleware#cors) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/cors" -) - -func main() { - app := fiber.New() - - // CORS with default config - app.Use(cors.New()) - - app.Listen(3000) -} -``` - -Check CORS by passing any domain in `Origin` header: - -```bash -curl -H "Origin: http://example.com" --verbose http://localhost:3000 -``` - -### Custom 404 response - -๐Ÿ“– [HTTP Methods](https://docs.gofiber.io/application#http-methods) - -```go -func main() { - app := fiber.New() - - app.Static("./public") - - app.Get("/demo", func(c *fiber.Ctx) { - c.Send("This is a demo!") - }) - - app.Post("/register", func(c *fiber.Ctx) { - c.Send("Welcome!") - }) - - // Last middleware to match anything - app.Use(func(c *fiber.Ctx) { - c.SendStatus(404) - // => 404 "Not Found" - }) - - app.Listen(3000) -} -``` - -### JSON Response - -๐Ÿ“– [JSON](https://docs.gofiber.io/context#json) - -```go -type User struct { - Name string `json:"name"` - Age int `json:"age"` -} - -func main() { - app := fiber.New() - - app.Get("/user", func(c *fiber.Ctx) { - c.JSON(&User{"John", 20}) - // => {"name":"John", "age":20} - }) - - app.Get("/json", func(c *fiber.Ctx) { - c.JSON(fiber.Map{ - "success": true, - "message": "Hi John!", - }) - // => {"success":true, "message":"Hi John!"} - }) - - app.Listen(3000) -} -``` - -### WebSocket Upgrade - -๐Ÿ“– [Websocket](https://docs.gofiber.io/middleware#websocket) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/websocket" -) - -func main() { - app := fiber.New() - - app.Get("/ws", websocket.New(func(c *websocket.Conn) { - for { - mt, msg, err := c.ReadMessage() - if err != nil { - log.Println("read:", err) - break - } - log.Printf("recv: %s", msg) - err = c.WriteMessage(mt, msg) - if err != nil { - log.Println("write:", err) - break - } - } - })) - - app.Listen(3000) - // ws://localhost:3000/ws -} -``` - -### Recover middleware - -๐Ÿ“– [Recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - app.Use(middleware.Recover()) - - app.Get("/", func(c *fiber.Ctx) { - panic("normally this would crash your app") - }) - - app.Listen(3000) -} -``` -
- -## ๐Ÿงฌ Fiber Middleware - -The Fiber middleware modules listed here are maintained by the [Fiber team](https://github.com/orgs/gofiber/people). - -| Middleware | Description | Built-in middleware | -| :--- | :--- | :--- | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | - | -| [basicauth](https://github.com/gofiber/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | - | -| [compress](https://github.com/gofiber/fiber/blob/master/middleware/compress.md) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | `middleware.Compress()` | -| [cors](https://github.com/gofiber/cors) | Enable cross-origin resource sharing \(CORS\) with various options. | - | -| [csrf](https://github.com/gofiber/csrf) | Protect from CSRF exploits. | - | -| [filesystem](https://github.com/gofiber/fiber/blob/master/middleware/filesystem.md) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | - | -| [favicon](https://github.com/gofiber/fiber/blob/master/middleware/favicon.md) | Ignore favicon from logs or serve from memory if a file path is provided. | `middleware.Favicon()` | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | - | -| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | - | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | - | -| [limiter](https://github.com/gofiber/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | - | -| [logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) | HTTP request/response logger. | `middleware.Logger()` | -| [pprof](https://github.com/gofiber/pprof) | Special thanks to Matthew Lee \(@mthli\) | - | -| [recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) | Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized[ ErrorHandler](error-handling.md). | `middleware.Recover()` | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | - | -| [requestid](https://github.com/gofiber/fiber/blob/master/middleware/request_id.md) | Request ID middleware generates a unique id for a request. | `middleware.RequestID()` | -| [session](https://github.com/gofiber/session) | This session middleware is build on top of fasthttp/session by @savsgio MIT. Special thanks to @thomasvvugt for helping with this middleware. | - | -| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | - | -| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | - | - -## ๐ŸŒฑ Third Party Middlewares - -This is a list of middlewares that are created by the Fiber community, please create a PR if you want to see yours! -- [arsmn/fiber-casbin](https://github.com/arsmn/fiber-casbin) -- [arsmn/fiber-introspect](https://github.com/arsmn/fiber-introspect) -- [arsmn/fiber-swagger](https://github.com/arsmn/fiber-swagger) -- [arsmn/gqlgen](https://github.com/arsmn/gqlgen) -- [codemicro/fiber-cache](https://github.com/codemicro/fiber-cache) -- [itsursujit/fiber-boilerplate](https://github.com/itsursujit/fiber-boilerplate) -- [juandiii/go-jwk-security](https://github.com/juandiii/go-jwk-security) -- [kiyonlin/fiber_limiter](https://github.com/kiyonlin/fiber_limiter) -- [shareed2k/fiber_limiter](https://github.com/shareed2k/fiber_limiter) -- [shareed2k/fiber_tracing](https://github.com/shareed2k/fiber_tracing) -- [thomasvvugt/fiber-boilerplate](https://github.com/thomasvvugt/fiber-boilerplate) - -## ๐Ÿ’ฌ ๋ฏธ๋””์–ด - -

- - -

- -- [Welcome to Fiber โ€” an Express.js styled web framework written in Go with โค๏ธ](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) โ€” _03 Feb 2020_ -- [Fiber released v1.7! ๐ŸŽ‰ What's new and is it still fast, flexible and friendly?](https://dev.to/koddr/fiber-v2-is-out-now-what-s-new-and-is-he-still-fast-flexible-and-friendly-3ipf) โ€” _21 Feb 2020_ -- [๐Ÿš€ Fiber v1.8. What's new, updated and re-thinked?](https://dev.to/koddr/fiber-v1-8-what-s-new-updated-and-re-thinked-339h) โ€” _03 Mar 2020_ -- [Is switching from Express to Fiber worth it? ๐Ÿค”](https://dev.to/koddr/are-sure-what-your-lovely-web-framework-running-so-fast-2jl1) โ€” _01 Apr 2020_ -- [Creating Fast APIs In Go Using Fiber](https://dev.to/jozsefsallai/creating-fast-apis-in-go-using-fiber-59m9) โ€” _07 Apr 2020_ -- [Building a Basic REST API in Go using Fiber](https://tutorialedge.net/golang/basic-rest-api-go-fiber/) - _23 Apr 2020_ -- [๐Ÿ“บ Building a REST API using GORM and Fiber](https://youtu.be/Iq2qT0fRhAA) - _25 Apr 2020_ -- [๐ŸŒŽ Create a travel list app with Go, Fiber, Angular, MongoDB and Google Cloud Secret Manager](https://blog.yongweilun.me/create-a-travel-list-app-with-go-fiber-angular-mongodb-and-google-cloud-secret-manager-ck9fgxy0p061pcss1xt1ubu8t) - _25 Apr 2020_ -- [Fiber v1.9.6 ๐Ÿ”ฅ How to improve performance by 817% and stay fast, flexible and friendly?](https://dev.to/koddr/fiber-v1-9-5-how-to-improve-performance-by-817-and-stay-fast-flexible-and-friendly-2dp6) - _12 May 2020_ -- [The road to web-based authentication with Fiber โšก](https://vugt.me/the-road-to-web-based-authentication-with-fiber/) - _20 May 2020_ -- [Building an Express-style API in Go with Fiber](https://blog.logrocket.com/express-style-api-go-fiber/) - _10 June 2020_ -- [ๅŸบไบŽgolang fiberๅ’Œangularๅผ€ๅ‘web](https://zhuanlan.zhihu.com/p/148925642) - _19 June 2020_ -- [ๅŸบไบŽๅปถ่ฟŸ่ฎก็ฎ—ไปค็‰Œๆกถ็š„gofiber้ข‘็Ž‡้™ๅˆถไธญ้—ดไปถๅฎž็Žฐ](https://zhuanlan.zhihu.com/p/149308936) - _20 June 2020_ -- [Construir una API en Golang con Fiber ๐Ÿ‡ช๐Ÿ‡ธ](https://enbonnet.me/article/53/construir-api-golang-con-fiber) - _28 June 2020_ -- [๐Ÿ“บWhy Go Fiber Is THE New Framework To Learn](https://www.youtube.com/watch?v=kvwsPeWDLM8) - _29 June 2020_ -- [่งฃๆžGofiber่ทฏ็”ฑ็ฎก็†](https://zhuanlan.zhihu.com/p/152494502) - _08 July 2020_ -- [๐Ÿ“บ Introduction to Fiber - An Express-inspired web framework](https://youtu.be/MfFi4Gt-tos) - _25 July 2020_ - -## ๐Ÿ‘ ๊ธฐ์—ฌ - -`Fiber`์˜ ํ™œ๋ฐœํ•œ ๊ฐœ๋ฐœ์„ ์ง€์›ํ•˜๊ณ  ๊ฐ์‚ฌ ์ธ์‚ฌ๋ฅผ ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด: - -1. ํ”„๋กœ์ ํŠธ์— [GitHub Star](https://github.com/gofiber/fiber/stargazers)๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”. -2. [ํŠธ์œ„ํ„ฐ์—์„œ](https://twitter.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber) ํ”„๋กœ์ ํŠธ์— ๋Œ€ํ•ด ํŠธ์œ—ํ•˜์„ธ์š”. -3. [Medium](https://medium.com/), [Dev.to](https://dev.to/) ๋˜๋Š” ๊ฐœ์ธ ๋ธ”๋กœ๊ทธ์— ๋ฆฌ๋ทฐ ๋˜๋Š” ํŠœํ† ๋ฆฌ์–ผ์„ ์ž‘์„ฑํ•˜์„ธ์š”. -4. Help us to translate our API Documentation via [Crowdin](https://crowdin.com/project/gofiber) [![Crowdin](https://badges.crowdin.net/gofiber/localized.svg)](https://crowdin.com/project/gofiber) -5. Support the project by donating a [cup of coffee](https://buymeacoff.ee/fenny). - -## โ˜• Supporters - -Fiber is an open source project that runs on donations to pay the bills e.g. our domain name, gitbook, netlify and serverless hosting. If you want to support Fiber, you can โ˜• [**buy a coffee here**](https://buymeacoff.ee/fenny). - -| | User | Donation | -| :---------------------------------------------------------- | :----------------------------------------------- | :-------- | -| ![](https://avatars.githubusercontent.com/u/204341?s=25 ) | [@destari](https://github.com/destari) | โ˜• x 10 | -| ![](https://avatars.githubusercontent.com/u/63164982?s=25 ) | [@dembygenesis](https://github.com/dembygenesis) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/56607882?s=25 ) | [@thomasvvugt](https://github.com/thomasvvugt) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/27820675?s=25 ) | [@hendratommy](https://github.com/hendratommy) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/1094221?s=25 ) | [@ekaputra07](https://github.com/ekaputra07) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/194590?s=25 ) | [@jorgefuertes](https://github.com/jorgefuertes) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/186637?s=25 ) | [@candidosales](https://github.com/candidosales) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/29659953?s=25 ) | [@l0nax](https://github.com/l0nax) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/59947262?s=25 ) | [@ankush](https://github.com/ankush) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/635852?s=25 ) | [@bihe](https://github.com/bihe) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/307334?s=25 ) | [@justdave](https://github.com/justdave) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/11155743?s=25 ) | [@koddr](https://github.com/koddr) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/29042462?s=25 ) | [@lapolinar](https://github.com/lapolinar) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/2978730?s=25 ) | [@diegowifi](https://github.com/diegowifi) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/44171355?s=25 ) | [@ssimk0](https://github.com/ssimk0) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/5638101?s=25 ) | [@raymayemir](https://github.com/raymayemir) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/619996?s=25 ) | [@melkorm](https://github.com/melkorm) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31022056?s=25 ) | [@marvinjwendt](https://github.com/thomasvvugt) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31921460?s=25 ) | [@toishy](https://github.com/toishy) | โ˜• x 1 | - -## โ€Žโ€๐Ÿ’ป Code Contributors - -Code Contributors - -## โญ๏ธ Stargazers - -Stargazers over time - -## โš ๏ธ License - -Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` is free and open-source software licensed under the [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Official logo was created by [Vic Shรณstak](https://github.com/koddr) and distributed under [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) license (CC BY-SA 4.0 International). - -**Third-party library licenses** -- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) -- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) -- [fasttemplate](https://github.com/valyala/fasttemplate/blob/master/LICENSE) -- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) diff --git a/.github/README_nl.md b/.github/README_nl.md deleted file mode 100644 index b92003a8..00000000 --- a/.github/README_nl.md +++ /dev/null @@ -1,595 +0,0 @@ -

- - Fiber - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -

-

- Fiber is een web framework geรฏnspireerd door Express gebouwd bovenop Fasthttp, de snelste HTTP-engine voor Go. Ontworpen om snelle ontwikkeling gemakkelijker te maken zonder geheugenallocatie tezamen met hoge prestaties. -

- -## โšก๏ธ Bliksemsnelle start - -```go -package main - -import "github.com/gofiber/fiber" - -func main() { - app := fiber.New() - - app.Get("/", func(c *fiber.Ctx) { - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -## ๐Ÿค– Benchmarks - -Deze tests zijn uitgevoerd door [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) en [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Bezoek onze [Wiki](https://fiber.wiki/benchmarks) voor alle benchmark resultaten. - -

- - -

- -## โš™๏ธ Installatie - -Allereerst, [download](https://golang.org/dl/) en installeer Go. `1.11` of hoger is vereist. - -Installatie wordt gedaan met behulp van het [`go get`](https://golang.org/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) commando: - -```bash -go get -u github.com/gofiber/fiber -``` - -## ๐ŸŽฏ Features - -- Robuuste [routing](https://fiber.wiki/routing) -- Serveer [statische bestanden](https://fiber.wiki/application#static) -- Extreme [prestaties](https://fiber.wiki/benchmarks) -- [Weinig geheugenruimte](https://fiber.wiki/benchmarks) -- [API endpoints](https://fiber.wiki/context) -- [Middleware](https://fiber.wiki/middleware) & [Next](https://fiber.wiki/context#next) ondersteuning -- [Snelle](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) server-side programmering -- [Template engines](https://fiber.wiki/middleware#template) -- [WebSocket ondersteuning](https://fiber.wiki/middleware#websocket) -- [Rate Limiter](https://fiber.wiki/middleware#limiter) -- Vertaald in [15 talen](https://docs.gofiber.io/) -- En nog veel meer, [ontdek Fiber](https://fiber.wiki/) - -## ๐Ÿ’ก Filosofie - -Nieuwe gophers die de overstap maken van [Node.js](https://nodejs.org/en/about/) naar [Go](https://golang.org/doc/), hebben te maken met een leercurve voordat ze kunnen beginnen met het bouwen van hun webapplicaties of microservices. Fiber, als een **web framework**, is gebouwd met het idee van **minimalisme** en volgt de **UNIX-manier**, zodat nieuwe gophers snel de wereld van Go kunnen betreden met een warm en vertrouwd welkom.\ - -Fiber is **geรฏnspireerd** door Express, het populairste webframework op internet. We hebben het **gemak** van Express gecombineerd met de **onbewerkte prestaties** van Go. Als je ooit een webapplicatie in Node.js hebt geรฏmplementeerd (_zoals Express of vergelijkbaar_), dan zullen veel methoden en principes **heel gewoon** voor je lijken. - -We **luisteren** naar onze gebruikers in [issues](https://github.com/gofiber/fiber/issues) (_en overal op het internet_) om een **snelle**, **flexibele** en **vriendelijk** Go web framework te maken voor **elke** taak, **deadline** en ontwikkelaar **vaardigheid**! Net zoals Express dat doet in de JavaScript-wereld. - -## ๐Ÿ‘€ Voorbeelden - -Hieronder staan enkele van de meest voorkomende voorbeelden. - -> Bekijk ons [Recepten repository](https://github.com/gofiber/recipes) voor meer voorbeelden met code of bezoek onze [API documentatie](https://fiber.wiki). - -Listed below are some of the common examples. If you want to see more code examples , please visit our [Recipes repository](https://github.com/gofiber/recipes) or visit our hosted [API documentation](https://docs.gofiber.io). - -#### ๐Ÿ“– [**Basic Routing**](https://docs.gofiber.io/#basic-routing) - -```go -func main() { - app := fiber.New() - - // GET /john - app.Get("/:name", func(c *fiber.Ctx) { - msg := fmt.Sprintf("Hello, %s ๐Ÿ‘‹!", c.Params("name")) - c.Send(msg) // => Hello john ๐Ÿ‘‹! - }) - - // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ‘ด %s is %s years old", c.Params("name"), c.Params("age")) - c.Send(msg) // => ๐Ÿ‘ด john is 75 years old - }) - - // GET /dictionary.txt - app.Get("/:file.:ext", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ“ƒ %s.%s", c.Params("file"), c.Params("ext")) - c.Send(msg) // => ๐Ÿ“ƒ dictionary.txt - }) - - // GET /flights/LAX-SFO - app.Get("/flights/:from-:to", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ’ธ From: %s, To: %s", c.Params("from"), c.Params("to")) - c.Send(msg) // => ๐Ÿ’ธ From: LAX, To: SFO - }) - - // GET /api/register - app.Get("/api/*", func(c *fiber.Ctx) { - msg := fmt.Sprintf("โœ‹ %s", c.Params("*")) - c.Send(msg) // => โœ‹ /api/register - }) - - app.Listen(3000) -} -``` - -#### ๐Ÿ“– [**Serving Static Files**](https://docs.gofiber.io/application#static) - -```go -func main() { - app := fiber.New() - - app.Static("/", "./public") - // => http://localhost:3000/js/script.js - // => http://localhost:3000/css/style.css - - app.Static("/prefix", "./public") - // => http://localhost:3000/prefix/js/script.js - // => http://localhost:3000/prefix/css/style.css - - app.Static("*", "./public/index.html") - // => http://localhost:3000/any/path/shows/index/html - - app.Listen(3000) -} -``` - -#### ๐Ÿ“– [**Middleware & Next**](https://docs.gofiber.io/context#next) - -```go -func main() { - app := fiber.New() - - // Match any route - app.Use(func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‡ First handler") - c.Next() - }) - - // Match all routes starting with /api - app.Use("/api", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅˆ Second handler") - c.Next() - }) - - // GET /api/register - app.Get("/api/list", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‰ Last handler") - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -
- ๐Ÿ“š Show more code examples - -### Views engines - -๐Ÿ“– [Settings](https://docs.gofiber.io/application#settings) -๐Ÿ“– [Engines](https://github.com/gofiber/template) -๐Ÿ“– [Render](https://docs.gofiber.io/context#render) - -Fiber defaults to the [html/template](https://golang.org/pkg/html/template/) when no view engine is set. - -If you want to execute partials or use a different engine like [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) or [pug](https://github.com/Joker/jade) etc.. - -Checkout our [Template](https://github.com/gofiber/template) package that support multiple view engines. - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/template/pug" -) - -func main() { - // You can setup Views engine before initiation app: - app := fiber.New(&fiber.Settings{ - Views: pug.New("./views", ".pug"), - }) - - // OR after initiation app at any convenient location: - app.Settings.Views = pug.New("./views", ".pug"), - - // And now, you can call template `./views/home.pug` like this: - app.Get("/", func(c *fiber.Ctx) { - c.Render("home", fiber.Map{ - "title": "Homepage", - "year": 1999, - }) - }) - - // ... -} -``` - -### Grouping routes into chains - -๐Ÿ“– [Group](https://docs.gofiber.io/application#group) - -```go -func main() { - app := fiber.New() - - // Root API route - api := app.Group("/api", cors()) // /api - - // API v1 routes - v1 := api.Group("/v1", mysql()) // /api/v1 - v1.Get("/list", handler) // /api/v1/list - v1.Get("/user", handler) // /api/v1/user - - // API v2 routes - v2 := api.Group("/v2", mongodb()) // /api/v2 - v2.Get("/list", handler) // /api/v2/list - v2.Get("/user", handler) // /api/v2/user - - // ... -} -``` - -### Middleware logger - -๐Ÿ“– [Logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - // Default - app.Use(middleware.Logger()) - - // Custom logging format - app.Use(middleware.Logger("${method} - ${path}")) - - // Custom Config - app.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ - Next: func(ctx *fiber.Ctx) bool { - return ctx.Path() != "/private" - }, - Format: "${method} - ${path}", - Output: io.Writer, - })) - - app.Listen(3000) -} -``` - -### Cross-Origin Resource Sharing (CORS) - -๐Ÿ“– [CORS](https://docs.gofiber.io/middleware#cors) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/cors" -) - -func main() { - app := fiber.New() - - // CORS with default config - app.Use(cors.New()) - - app.Listen(3000) -} -``` - -Check CORS by passing any domain in `Origin` header: - -```bash -curl -H "Origin: http://example.com" --verbose http://localhost:3000 -``` - -### Custom 404 response - -๐Ÿ“– [HTTP Methods](https://docs.gofiber.io/application#http-methods) - -```go -func main() { - app := fiber.New() - - app.Static("./public") - - app.Get("/demo", func(c *fiber.Ctx) { - c.Send("This is a demo!") - }) - - app.Post("/register", func(c *fiber.Ctx) { - c.Send("Welcome!") - }) - - // Last middleware to match anything - app.Use(func(c *fiber.Ctx) { - c.SendStatus(404) - // => 404 "Not Found" - }) - - app.Listen(3000) -} -``` - -### JSON Response - -๐Ÿ“– [JSON](https://docs.gofiber.io/context#json) - -```go -type User struct { - Name string `json:"name"` - Age int `json:"age"` -} - -func main() { - app := fiber.New() - - app.Get("/user", func(c *fiber.Ctx) { - c.JSON(&User{"John", 20}) - // => {"name":"John", "age":20} - }) - - app.Get("/json", func(c *fiber.Ctx) { - c.JSON(fiber.Map{ - "success": true, - "message": "Hi John!", - }) - // => {"success":true, "message":"Hi John!"} - }) - - app.Listen(3000) -} -``` - -### WebSocket Upgrade - -๐Ÿ“– [Websocket](https://docs.gofiber.io/middleware#websocket) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/websocket" -) - -func main() { - app := fiber.New() - - app.Get("/ws", websocket.New(func(c *websocket.Conn) { - for { - mt, msg, err := c.ReadMessage() - if err != nil { - log.Println("read:", err) - break - } - log.Printf("recv: %s", msg) - err = c.WriteMessage(mt, msg) - if err != nil { - log.Println("write:", err) - break - } - } - })) - - app.Listen(3000) - // ws://localhost:3000/ws -} -``` - -### Recover middleware - -๐Ÿ“– [Recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - app.Use(middleware.Recover()) - - app.Get("/", func(c *fiber.Ctx) { - panic("normally this would crash your app") - }) - - app.Listen(3000) -} -``` -
- -## ๐Ÿงฌ Fiber Middleware - -The Fiber middleware modules listed here are maintained by the [Fiber team](https://github.com/orgs/gofiber/people). - -| Middleware | Description | Built-in middleware | -| :--- | :--- | :--- | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | - | -| [basicauth](https://github.com/gofiber/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | - | -| [compress](https://github.com/gofiber/fiber/blob/master/middleware/compress.md) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | `middleware.Compress()` | -| [cors](https://github.com/gofiber/cors) | Enable cross-origin resource sharing \(CORS\) with various options. | - | -| [csrf](https://github.com/gofiber/csrf) | Protect from CSRF exploits. | - | -| [filesystem](https://github.com/gofiber/fiber/blob/master/middleware/filesystem.md) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | - | -| [favicon](https://github.com/gofiber/fiber/blob/master/middleware/favicon.md) | Ignore favicon from logs or serve from memory if a file path is provided. | `middleware.Favicon()` | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | - | -| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | - | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | - | -| [limiter](https://github.com/gofiber/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | - | -| [logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) | HTTP request/response logger. | `middleware.Logger()` | -| [pprof](https://github.com/gofiber/pprof) | Special thanks to Matthew Lee \(@mthli\) | - | -| [recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) | Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized[ ErrorHandler](error-handling.md). | `middleware.Recover()` | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | - | -| [requestid](https://github.com/gofiber/fiber/blob/master/middleware/request_id.md) | Request ID middleware generates a unique id for a request. | `middleware.RequestID()` | -| [session](https://github.com/gofiber/session) | This session middleware is build on top of fasthttp/session by @savsgio MIT. Special thanks to @thomasvvugt for helping with this middleware. | - | -| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | - | -| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | - | - -## ๐ŸŒฑ Third Party Middlewares - -This is a list of middlewares that are created by the Fiber community, please create a PR if you want to see yours! -- [arsmn/fiber-casbin](https://github.com/arsmn/fiber-casbin) -- [arsmn/fiber-introspect](https://github.com/arsmn/fiber-introspect) -- [arsmn/fiber-swagger](https://github.com/arsmn/fiber-swagger) -- [arsmn/gqlgen](https://github.com/arsmn/gqlgen) -- [codemicro/fiber-cache](https://github.com/codemicro/fiber-cache) -- [itsursujit/fiber-boilerplate](https://github.com/itsursujit/fiber-boilerplate) -- [juandiii/go-jwk-security](https://github.com/juandiii/go-jwk-security) -- [kiyonlin/fiber_limiter](https://github.com/kiyonlin/fiber_limiter) -- [shareed2k/fiber_limiter](https://github.com/shareed2k/fiber_limiter) -- [shareed2k/fiber_tracing](https://github.com/shareed2k/fiber_tracing) -- [thomasvvugt/fiber-boilerplate](https://github.com/thomasvvugt/fiber-boilerplate) - -## ๐Ÿ’ฌ Media - -

- - -

- -- [Welcome to Fiber โ€” an Express.js styled web framework written in Go with โค๏ธ](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) โ€” _03 Feb 2020_ -- [Fiber released v1.7! ๐ŸŽ‰ What's new and is it still fast, flexible and friendly?](https://dev.to/koddr/fiber-v2-is-out-now-what-s-new-and-is-he-still-fast-flexible-and-friendly-3ipf) โ€” _21 Feb 2020_ -- [๐Ÿš€ Fiber v1.8. What's new, updated and re-thinked?](https://dev.to/koddr/fiber-v1-8-what-s-new-updated-and-re-thinked-339h) โ€” _03 Mar 2020_ -- [Is switching from Express to Fiber worth it? ๐Ÿค”](https://dev.to/koddr/are-sure-what-your-lovely-web-framework-running-so-fast-2jl1) โ€” _01 Apr 2020_ -- [Creating Fast APIs In Go Using Fiber](https://dev.to/jozsefsallai/creating-fast-apis-in-go-using-fiber-59m9) โ€” _07 Apr 2020_ -- [Building a Basic REST API in Go using Fiber](https://tutorialedge.net/golang/basic-rest-api-go-fiber/) - _23 Apr 2020_ -- [๐Ÿ“บ Building a REST API using GORM and Fiber](https://youtu.be/Iq2qT0fRhAA) - _25 Apr 2020_ -- [๐ŸŒŽ Create a travel list app with Go, Fiber, Angular, MongoDB and Google Cloud Secret Manager](https://blog.yongweilun.me/create-a-travel-list-app-with-go-fiber-angular-mongodb-and-google-cloud-secret-manager-ck9fgxy0p061pcss1xt1ubu8t) - _25 Apr 2020_ -- [Fiber v1.9.6 ๐Ÿ”ฅ How to improve performance by 817% and stay fast, flexible and friendly?](https://dev.to/koddr/fiber-v1-9-5-how-to-improve-performance-by-817-and-stay-fast-flexible-and-friendly-2dp6) - _12 May 2020_ -- [The road to web-based authentication with Fiber โšก](https://vugt.me/the-road-to-web-based-authentication-with-fiber/) - _20 May 2020_ -- [Building an Express-style API in Go with Fiber](https://blog.logrocket.com/express-style-api-go-fiber/) - _10 June 2020_ -- [ๅŸบไบŽgolang fiberๅ’Œangularๅผ€ๅ‘web](https://zhuanlan.zhihu.com/p/148925642) - _19 June 2020_ -- [ๅŸบไบŽๅปถ่ฟŸ่ฎก็ฎ—ไปค็‰Œๆกถ็š„gofiber้ข‘็Ž‡้™ๅˆถไธญ้—ดไปถๅฎž็Žฐ](https://zhuanlan.zhihu.com/p/149308936) - _20 June 2020_ -- [Construir una API en Golang con Fiber ๐Ÿ‡ช๐Ÿ‡ธ](https://enbonnet.me/article/53/construir-api-golang-con-fiber) - _28 June 2020_ -- [๐Ÿ“บWhy Go Fiber Is THE New Framework To Learn](https://www.youtube.com/watch?v=kvwsPeWDLM8) - _29 June 2020_ -- [่งฃๆžGofiber่ทฏ็”ฑ็ฎก็†](https://zhuanlan.zhihu.com/p/152494502) - _08 July 2020_ -- [๐Ÿ“บ Introduction to Fiber - An Express-inspired web framework](https://youtu.be/MfFi4Gt-tos) - _25 July 2020_ - -## ๐Ÿ‘ Bijdragen - -Om de actieve ontwikkelingen van `Fiber` te ondersteunen of om een **bedankje** te geven: - -1. Voeg een [GitHub Star](https://github.com/gofiber/fiber/stargazers) toe aan het project. -2. Tweet over het project [op je Twitter account](https://twitter.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). -3. Schrijf een recensie of tutorial op [Medium](https://medium.com/), [Dev.to](https://dev.to/) of een persoonlijke blog. -4. Help us to translate our API Documentation via [Crowdin](https://crowdin.com/project/gofiber) [![Crowdin](https://badges.crowdin.net/gofiber/localized.svg)](https://crowdin.com/project/gofiber) -5. Support the project by donating a [cup of coffee](https://buymeacoff.ee/fenny). - -## โ˜• Supporters - -Fiber is an open source project that runs on donations to pay the bills e.g. our domain name, gitbook, netlify and serverless hosting. If you want to support Fiber, you can โ˜• [**buy a coffee here**](https://buymeacoff.ee/fenny). - -| | User | Donation | -| :---------------------------------------------------------- | :----------------------------------------------- | :-------- | -| ![](https://avatars.githubusercontent.com/u/204341?s=25 ) | [@destari](https://github.com/destari) | โ˜• x 10 | -| ![](https://avatars.githubusercontent.com/u/63164982?s=25 ) | [@dembygenesis](https://github.com/dembygenesis) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/56607882?s=25 ) | [@thomasvvugt](https://github.com/thomasvvugt) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/27820675?s=25 ) | [@hendratommy](https://github.com/hendratommy) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/1094221?s=25 ) | [@ekaputra07](https://github.com/ekaputra07) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/194590?s=25 ) | [@jorgefuertes](https://github.com/jorgefuertes) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/186637?s=25 ) | [@candidosales](https://github.com/candidosales) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/29659953?s=25 ) | [@l0nax](https://github.com/l0nax) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/59947262?s=25 ) | [@ankush](https://github.com/ankush) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/635852?s=25 ) | [@bihe](https://github.com/bihe) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/307334?s=25 ) | [@justdave](https://github.com/justdave) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/11155743?s=25 ) | [@koddr](https://github.com/koddr) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/29042462?s=25 ) | [@lapolinar](https://github.com/lapolinar) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/2978730?s=25 ) | [@diegowifi](https://github.com/diegowifi) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/44171355?s=25 ) | [@ssimk0](https://github.com/ssimk0) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/5638101?s=25 ) | [@raymayemir](https://github.com/raymayemir) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/619996?s=25 ) | [@melkorm](https://github.com/melkorm) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31022056?s=25 ) | [@marvinjwendt](https://github.com/thomasvvugt) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31921460?s=25 ) | [@toishy](https://github.com/toishy) | โ˜• x 1 | - -## โ€Žโ€๐Ÿ’ป Code Contributors - -Code Contributors - -## โญ๏ธ Stargazers - -Stargazers over time - -## โš ๏ธ License - -Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` is free and open-source software licensed under the [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Official logo was created by [Vic Shรณstak](https://github.com/koddr) and distributed under [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) license (CC BY-SA 4.0 International). - -**Third-party library licenses** -- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) -- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) -- [fasttemplate](https://github.com/valyala/fasttemplate/blob/master/LICENSE) -- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) diff --git a/.github/README_pt.md b/.github/README_pt.md deleted file mode 100644 index 3f694df5..00000000 --- a/.github/README_pt.md +++ /dev/null @@ -1,591 +0,0 @@ -

- - Fiber - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -

-

-Fiber รฉ um framework web inspirado no Express, construรญdo sobre o Fasthttp, o motor HTTP mais rรกpido do Go. Projetado para facilitar e acelerar o desenvolvimento, com zero de alocaรงรฃo de memรณria e desempenho em mente. -

- -## โšก๏ธ Inรญcio rรกpido - -```go -package main - -import "github.com/gofiber/fiber" - -func main() { - app := fiber.New() - - app.Get("/", func(c *fiber.Ctx) { - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -## ๐Ÿค– Benchmarks - -Esses testes sรฃo realizados pelo [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) e [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Se vocรช quiser ver todos os resultados, visite nosso [Wiki](https://docs.gofiber.io/benchmarks) . - -

- - -

- -## โš™๏ธ Instalaรงรฃo - -Primeiro de tudo, faรงa o [download](https://golang.org/dl/) e instale o Go. ร‰ necessรกrio a versรฃo `1.11` ou superior. - -A instalaรงรฃo รฉ feita usando o comando [`go get`](https://golang.org/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) : - -```bash -go get -u github.com/gofiber/fiber/... -``` - -## ๐ŸŽฏ Recursos - -- [Roteamento](https://docs.gofiber.io/routing) robusto -- Servir [arquivos estรกticos](https://docs.gofiber.io/application#static) -- [Desempenho](https://docs.gofiber.io/benchmarks) extremo -- [Baixo consumo de memรณria](https://docs.gofiber.io/benchmarks) -- [API de rotas](https://docs.gofiber.io/context) -- Suporte ร  Middleware e [Next](https://docs.gofiber.io/context#next) -- Programaรงรฃo [rรกpida](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) de aplicaรงรตes de servidor -- [Templates](https://github.com/gofiber/template) -- [Suporte ร  WebSockets](https://docs.gofiber.io/middleware#websocket) -- [Limitador de requisiรงรตes](https://docs.gofiber.io/middleware#limiter) -- Disponรญvel em [15 lรญnguas](https://docs.gofiber.io/) -- E muito mais, [explore o Fiber](https://docs.gofiber.io/) - -## ๐Ÿ’ก Filosofia - -Os novos gophers que mudaram do [Node.js](https://nodejs.org/en/about/) para o [Go](https://golang.org/doc/) estรฃo tendo que lidar com uma curva de aprendizado antes que possam comeรงar a criar seus aplicativos web ou microsserviรงos. O Fiber, como um **framework web**, foi criado com a ideia de ser **minimalista** e seguindo a **filosofia UNIX**, para que novos gophers possam, rapidamente, entrar no mundo do Go com uma recepรงรฃo calorosa e confiรกvel. - -O Fiber รฉ **inspirado** no Express, o framework web mais popular da Internet. Combinamos a **facilidade** do Express e com o **desempenho bruto** do Go. Se vocรช jรก implementou um aplicativo web com Node.js ( _usando Express.js ou similar_ ), entรฃo muitos mรฉtodos e princรญpios parecerรฃo **muito familiares** para vocรช. - -## ๐Ÿ‘€ Exemplos - -Listados abaixo estรฃo alguns exemplos comuns. Se vocรช quiser ver mais exemplos de cรณdigo, -visite nosso [repositรณrio de receitas](https://github.com/gofiber/recipes) ou -a [documentaรงรฃo da API](https://docs.gofiber.io). - -#### ๐Ÿ“– [**Roteamento bรกsico**](https://docs.gofiber.io/#basic-routing) - -```go -func main() { - app := fiber.New() - - // GET /john - app.Get("/:name", func(c *fiber.Ctx) { - msg := fmt.Sprintf("Hello, %s ๐Ÿ‘‹!", c.Params("name")) - c.Send(msg) // => Hello john ๐Ÿ‘‹! - }) - - // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ‘ด %s is %s years old", c.Params("name"), c.Params("age")) - c.Send(msg) // => ๐Ÿ‘ด john is 75 years old - }) - - // GET /dictionary.txt - app.Get("/:file.:ext", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ“ƒ %s.%s", c.Params("file"), c.Params("ext")) - c.Send(msg) // => ๐Ÿ“ƒ dictionary.txt - }) - - // GET /flights/LAX-SFO - app.Get("/flights/:from-:to", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ’ธ From: %s, To: %s", c.Params("from"), c.Params("to")) - c.Send(msg) // => ๐Ÿ’ธ From: LAX, To: SFO - }) - - // GET /api/register - app.Get("/api/*", func(c *fiber.Ctx) { - msg := fmt.Sprintf("โœ‹ %s", c.Params("*")) - c.Send(msg) // => โœ‹ /api/register - }) - - app.Listen(3000) -} -``` - -#### ๐Ÿ“– [**Servindo arquivos estรกticos**](https://docs.gofiber.io/application#static) - -```go -func main() { - app := fiber.New() - - app.Static("/", "./public") - // => http://localhost:3000/js/script.js - // => http://localhost:3000/css/style.css - - app.Static("/prefix", "./public") - // => http://localhost:3000/prefix/js/script.js - // => http://localhost:3000/prefix/css/style.css - - app.Static("*", "./public/index.html") - // => http://localhost:3000/any/path/shows/index/html - - app.Listen(3000) -} -``` - -#### ๐Ÿ“– [**Middleware & Next**](https://docs.gofiber.io/context#next) - -```go -func main() { - app := fiber.New() - - // Match any route - app.Use(func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‡ First handler") - c.Next() - }) - - // Match all routes starting with /api - app.Use("/api", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅˆ Second handler") - c.Next() - }) - - // GET /api/register - app.Get("/api/list", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‰ Last handler") - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -
- ๐Ÿ“š Mostrar mais exemplos - -### Engines de visualizaรงรฃo - -๐Ÿ“– [Settings](https://docs.gofiber.io/application#settings) -๐Ÿ“– [Engines](https://github.com/gofiber/template) -๐Ÿ“– [Render](https://docs.gofiber.io/context#render) - -O Fiber usa por padrรฃo o [html/template](https://golang.org/pkg/html/template/) quando nenhuma engine รฉ selecionada. - -Se vocรช quiser uma execuรงรฃo parcial ou usar uma engine diferente como [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) ou [pug](https://github.com/Joker/jade) etc.. Dรช uma olhada no package [Template](https://github.com/gofiber/template) que suporta multiplas engines de visualizaรงรฃo. - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/template/pug" -) - -func main() { - // You can setup Views engine before initiation app: - app := fiber.New(&fiber.Settings{ - Views: pug.New("./views", ".pug"), - }) - - // OR after initiation app at any convenient location: - app.Settings.Views = pug.New("./views", ".pug"), - - // And now, you can call template `./views/home.pug` like this: - app.Get("/", func(c *fiber.Ctx) { - c.Render("home", fiber.Map{ - "title": "Homepage", - "year": 1999, - }) - }) - - // ... -} -``` - -### Agrupamento de rotas - -๐Ÿ“– [Group](https://docs.gofiber.io/application#group) - -```go -func main() { - app := fiber.New() - - // Root API route - api := app.Group("/api", cors()) // /api - - // API v1 routes - v1 := api.Group("/v1", mysql()) // /api/v1 - v1.Get("/list", handler) // /api/v1/list - v1.Get("/user", handler) // /api/v1/user - - // API v2 routes - v2 := api.Group("/v2", mongodb()) // /api/v2 - v2.Get("/list", handler) // /api/v2/list - v2.Get("/user", handler) // /api/v2/user - - // ... -} -``` - -### Middleware Logger - -๐Ÿ“– [Logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - // Default - app.Use(middleware.Logger()) - - // Custom logging format - app.Use(middleware.Logger("${method} - ${path}")) - - // Custom Config - app.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ - Next: func(ctx *fiber.Ctx) bool { - return ctx.Path() != "/private" - }, - Format: "${method} - ${path}", - Output: io.Writer, - })) - - app.Listen(3000) -} -``` - -### Cross-Origin Resource Sharing (CORS) - -๐Ÿ“– [CORS](https://docs.gofiber.io/middleware#cors) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/cors" -) - -func main() { - app := fiber.New() - - // CORS with default config - app.Use(cors.New()) - - app.Listen(3000) -} -``` - -Verifique o CORS passando qualquer domรญnio no header `Origin`: - -```bash -curl -H "Origin: http://example.com" --verbose http://localhost:3000 -``` - -### Resposta 404 customizada - -๐Ÿ“– [HTTP Methods](https://docs.gofiber.io/application#http-methods) - -```go -func main() { - app := fiber.New() - - app.Static("./public") - - app.Get("/demo", func(c *fiber.Ctx) { - c.Send("This is a demo!") - }) - - app.Post("/register", func(c *fiber.Ctx) { - c.Send("Welcome!") - }) - - // Last middleware to match anything - app.Use(func(c *fiber.Ctx) { - c.SendStatus(404) - // => 404 "Not Found" - }) - - app.Listen(3000) -} -``` - -### Resposta JSON - -๐Ÿ“– [JSON](https://docs.gofiber.io/context#json) - -```go -type User struct { - Name string `json:"name"` - Age int `json:"age"` -} - -func main() { - app := fiber.New() - - app.Get("/user", func(c *fiber.Ctx) { - c.JSON(&User{"John", 20}) - // => {"name":"John", "age":20} - }) - - app.Get("/json", func(c *fiber.Ctx) { - c.JSON(fiber.Map{ - "success": true, - "message": "Hi John!", - }) - // => {"success":true, "message":"Hi John!"} - }) - - app.Listen(3000) -} -``` - -### WebSocket - -๐Ÿ“– [Websocket](https://docs.gofiber.io/middleware#websocket) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/websocket" -) - -func main() { - app := fiber.New() - - app.Get("/ws", websocket.New(func(c *websocket.Conn) { - for { - mt, msg, err := c.ReadMessage() - if err != nil { - log.Println("read:", err) - break - } - log.Printf("recv: %s", msg) - err = c.WriteMessage(mt, msg) - if err != nil { - log.Println("write:", err) - break - } - } - })) - - app.Listen(3000) - // ws://localhost:3000/ws -} -``` - -### Middleware Recover - -๐Ÿ“– [Recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - app.Use(middleware.Recover()) - - app.Get("/", func(c *fiber.Ctx) { - panic("normally this would crash your app") - }) - - app.Listen(3000) -} -``` -
- -## ๐ŸงฌMiddlewares Fiber - -Os middlewares Fiber listados aqui sรฃo mantidos pelo [Time Fiber](https://github.com/orgs/gofiber/people). - -| Middleware | Description | Built-in middleware | -| :--- | :--- | :--- | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | - | -| [basicauth](https://github.com/gofiber/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | - | -| [compress](https://github.com/gofiber/fiber/blob/master/middleware/compress.md) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | `middleware.Compress()` | -| [cors](https://github.com/gofiber/cors) | Enable cross-origin resource sharing \(CORS\) with various options. | - | -| [csrf](https://github.com/gofiber/csrf) | Protect from CSRF exploits. | - | -| [filesystem](https://github.com/gofiber/fiber/blob/master/middleware/filesystem.md) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | - | -| [favicon](https://github.com/gofiber/fiber/blob/master/middleware/favicon.md) | Ignore favicon from logs or serve from memory if a file path is provided. | `middleware.Favicon()` | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | - | -| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | - | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | - | -| [limiter](https://github.com/gofiber/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | - | -| [logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) | HTTP request/response logger. | `middleware.Logger()` | -| [pprof](https://github.com/gofiber/pprof) | Special thanks to Matthew Lee \(@mthli\) | - | -| [recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) | Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized[ ErrorHandler](error-handling.md). | `middleware.Recover()` | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | - | -| [requestid](https://github.com/gofiber/fiber/blob/master/middleware/request_id.md) | Request ID middleware generates a unique id for a request. | `middleware.RequestID()` | -| [session](https://github.com/gofiber/session) | This session middleware is build on top of fasthttp/session by @savsgio MIT. Special thanks to @thomasvvugt for helping with this middleware. | - | -| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | - | -| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | - | - -## ๐ŸŒฑ Middlewares Third Party - -Esta รฉ uma lista de middlewares criados pela comunidade do Fiber, se quiser ter o seu middleware aqui, รฉ sรณ abrir um PR! -- [arsmn/fiber-casbin](https://github.com/arsmn/fiber-casbin) -- [arsmn/fiber-introspect](https://github.com/arsmn/fiber-introspect) -- [arsmn/fiber-swagger](https://github.com/arsmn/fiber-swagger) -- [arsmn/gqlgen](https://github.com/arsmn/gqlgen) -- [codemicro/fiber-cache](https://github.com/codemicro/fiber-cache) -- [itsursujit/fiber-boilerplate](https://github.com/itsursujit/fiber-boilerplate) -- [juandiii/go-jwk-security](https://github.com/juandiii/go-jwk-security) -- [kiyonlin/fiber_limiter](https://github.com/kiyonlin/fiber_limiter) -- [shareed2k/fiber_limiter](https://github.com/shareed2k/fiber_limiter) -- [shareed2k/fiber_tracing](https://github.com/shareed2k/fiber_tracing) -- [thomasvvugt/fiber-boilerplate](https://github.com/thomasvvugt/fiber-boilerplate) - -## ๐Ÿ’ฌ Mรญdia - -

- - -

- -- [Welcome to Fiber โ€” an Express.js styled web framework written in Go with โค๏ธ](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) โ€” _03 Feb 2020_ -- [Fiber released v1.7! ๐ŸŽ‰ What's new and is it still fast, flexible and friendly?](https://dev.to/koddr/fiber-v2-is-out-now-what-s-new-and-is-he-still-fast-flexible-and-friendly-3ipf) โ€” _21 Feb 2020_ -- [๐Ÿš€ Fiber v1.8. What's new, updated and re-thinked?](https://dev.to/koddr/fiber-v1-8-what-s-new-updated-and-re-thinked-339h) โ€” _03 Mar 2020_ -- [Is switching from Express to Fiber worth it? ๐Ÿค”](https://dev.to/koddr/are-sure-what-your-lovely-web-framework-running-so-fast-2jl1) โ€” _01 Apr 2020_ -- [Creating Fast APIs In Go Using Fiber](https://dev.to/jozsefsallai/creating-fast-apis-in-go-using-fiber-59m9) โ€” _07 Apr 2020_ -- [Building a Basic REST API in Go using Fiber](https://tutorialedge.net/golang/basic-rest-api-go-fiber/) - _23 Apr 2020_ -- [๐Ÿ“บ Building a REST API using GORM and Fiber](https://youtu.be/Iq2qT0fRhAA) - _25 Apr 2020_ -- [๐ŸŒŽ Create a travel list app with Go, Fiber, Angular, MongoDB and Google Cloud Secret Manager](https://blog.yongweilun.me/create-a-travel-list-app-with-go-fiber-angular-mongodb-and-google-cloud-secret-manager-ck9fgxy0p061pcss1xt1ubu8t) - _25 Apr 2020_ -- [Fiber v1.9.6 ๐Ÿ”ฅ How to improve performance by 817% and stay fast, flexible and friendly?](https://dev.to/koddr/fiber-v1-9-5-how-to-improve-performance-by-817-and-stay-fast-flexible-and-friendly-2dp6) - _12 May 2020_ -- [The road to web-based authentication with Fiber โšก](https://vugt.me/the-road-to-web-based-authentication-with-fiber/) - _20 May 2020_ -- [Building an Express-style API in Go with Fiber](https://blog.logrocket.com/express-style-api-go-fiber/) - _10 June 2020_ -- [ๅŸบไบŽgolang fiberๅ’Œangularๅผ€ๅ‘web](https://zhuanlan.zhihu.com/p/148925642) - _19 June 2020_ -- [ๅŸบไบŽๅปถ่ฟŸ่ฎก็ฎ—ไปค็‰Œๆกถ็š„gofiber้ข‘็Ž‡้™ๅˆถไธญ้—ดไปถๅฎž็Žฐ](https://zhuanlan.zhihu.com/p/149308936) - _20 June 2020_ -- [Construir una API en Golang con Fiber ๐Ÿ‡ช๐Ÿ‡ธ](https://enbonnet.me/article/53/construir-api-golang-con-fiber) - _28 June 2020_ -- [๐Ÿ“บWhy Go Fiber Is THE New Framework To Learn](https://www.youtube.com/watch?v=kvwsPeWDLM8) - _29 June 2020_ -- [่งฃๆžGofiber่ทฏ็”ฑ็ฎก็†](https://zhuanlan.zhihu.com/p/152494502) - _08 July 2020_ -- [๐Ÿ“บ Introduction to Fiber - An Express-inspired web framework](https://youtu.be/MfFi4Gt-tos) - _25 July 2020_ - -## ๐Ÿ‘ Contribuindo - -Se vocรช quer **agradecer** e/ou apoiar o desenvolvimento ativo do `Fiber`: - -1. Deixe uma [estrela no GitHub](https://github.com/gofiber/fiber/stargazers) do projeto. -2. Tweet sobre o projeto [no seu Twitter](https://twitter.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). -3. Escreva um review ou tutorial no [Medium](https://medium.com/), [Dev.to](https://dev.to/) ou blog pessoal. -4. Nos ajude a traduzir a documentaรงรฃo da API no [Crowdin](https://crowdin.com/project/gofiber) [![Crowdin](https://badges.crowdin.net/gofiber/localized.svg)](https://crowdin.com/project/gofiber) -5. Apoie o projeto pagando uma [xรญcara de cafรฉ](https://buymeacoff.ee/fenny). - -## โ˜• Apoiadores - -Fiber รฉ um projeto open source que usa de doaรงรตes para pagar seus custos (domรญnio, GitBook, Netlify e hospedagem serverless). Se vocรช quiser apoiar o projeto, vocรช pode โ˜• [**pagar um cafรฉ**](https://buymeacoff.ee/fenny). - -| | User | Donation | -| :---------------------------------------------------------- | :----------------------------------------------- | :-------- | -| ![](https://avatars.githubusercontent.com/u/204341?s=25 ) | [@destari](https://github.com/destari) | โ˜• x 10 | -| ![](https://avatars.githubusercontent.com/u/63164982?s=25 ) | [@dembygenesis](https://github.com/dembygenesis) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/56607882?s=25 ) | [@thomasvvugt](https://github.com/thomasvvugt) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/27820675?s=25 ) | [@hendratommy](https://github.com/hendratommy) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/1094221?s=25 ) | [@ekaputra07](https://github.com/ekaputra07) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/194590?s=25 ) | [@jorgefuertes](https://github.com/jorgefuertes) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/186637?s=25 ) | [@candidosales](https://github.com/candidosales) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/29659953?s=25 ) | [@l0nax](https://github.com/l0nax) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/59947262?s=25 ) | [@ankush](https://github.com/ankush) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/635852?s=25 ) | [@bihe](https://github.com/bihe) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/307334?s=25 ) | [@justdave](https://github.com/justdave) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/11155743?s=25 ) | [@koddr](https://github.com/koddr) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/29042462?s=25 ) | [@lapolinar](https://github.com/lapolinar) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/2978730?s=25 ) | [@diegowifi](https://github.com/diegowifi) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/44171355?s=25 ) | [@ssimk0](https://github.com/ssimk0) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/5638101?s=25 ) | [@raymayemir](https://github.com/raymayemir) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/619996?s=25 ) | [@melkorm](https://github.com/melkorm) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31022056?s=25 ) | [@marvinjwendt](https://github.com/thomasvvugt) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31921460?s=25 ) | [@toishy](https://github.com/toishy) | โ˜• x 1 | - -## โ€Žโ€๐Ÿ’ป Contribuidores de cรณdigo - -Code Contributors - -## โญ๏ธ Stargazers - -Stargazers over time - -## โš ๏ธ Licenรงa - -Todos os direitos reservados (c) 2019-presente [Fenny](https://github.com/fenny) e [Contribuidores](https://github.com/gofiber/fiber/graphs/contributors). -`Fiber` รฉ software livre e aberto sob a [licenรงa MIT](https://github.com/gofiber/fiber/blob/master/LICENSE). -O logo oficial foi criado por [Vic Shรณstak](https://github.com/koddr) e distribuรญdo sob a licenรงa [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) (CC BY-SA 4.0 International). - -**Licenรงa das bibliotecas de terceiros** -- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) -- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) -- [fasttemplate](https://github.com/valyala/fasttemplate/blob/master/LICENSE) -- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) diff --git a/.github/README_ru.md b/.github/README_ru.md deleted file mode 100644 index 12d841eb..00000000 --- a/.github/README_ru.md +++ /dev/null @@ -1,596 +0,0 @@ -

- - Fiber - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -

-

- Fiber โ€” ัั‚ะพ ะฒะตะฑ ั„ั€ะตะนะผะฒะพั€ะบ, ะบะพั‚ะพั€ั‹ะน ะฑั‹ะป ะฒะดะพั…ะฝะพะฒะปะตะฝ Express ะธ ะพัะฝะพะฒะฐะฝ ะฝะฐ Fasthttp, ัะฐะผะพะผ ะฑั‹ัั‚ั€ะพะผ HTTP-ะดะฒะธะถะบะต ะฝะฐะฟะธัะฐะฝะฝะพะผ ะฝะฐ Go. ะคั€ะตะนะผะฒะพั€ะบ ะฑั‹ะป ั€ะฐะทั€ะฐะฑะพั‚ะฐะฝ ั ั†ะตะปัŒัŽ ัƒะฟั€ะพัั‚ะธั‚ัŒ ะฟั€ะพั†ะตัั ะฑั‹ัั‚ั€ะพะน ั€ะฐะทั€ะฐะฑะพั‚ะบะธ ะฒั‹ัะพะบะพะฟั€ะพะธะทะฒะพะดะธั‚ะตะปัŒะฝั‹ั… ะฒะตะฑ-ะฟั€ะธะปะพะถะตะฝะธะน ั ะฝัƒะปะตะฒั‹ะผ ั€ะฐัะฟั€ะตะดะตะปะตะฝะธะตะผ ะฟะฐะผัั‚ะธ. -

- -## โšก๏ธ ะ‘ั‹ัั‚ั€ั‹ะน ัั‚ะฐั€ั‚ - -```go -package main - -import "github.com/gofiber/fiber" - -func main() { - app := fiber.New() - - app.Get("/", func(c *fiber.Ctx) { - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -## ๐Ÿค– ะ‘ะตะฝั‡ะผะฐั€ะบะธ - -ะขะตัั‚ะธั€ะพะฒะฐะฝะธะต ะฟั€ะพะฒะพะดะธะปะพััŒ ั ะฟะพะผะพั‰ัŒัŽ [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) ะธ [Go Web](https://github.com/smallnest/go-web-framework-benchmark). ะ•ัะปะธ ะฒั‹ ั…ะพั‚ะธั‚ะต ัƒะฒะธะดะตั‚ัŒ ะฒัะต ั€ะตะทัƒะปัŒั‚ะฐั‚ั‹, ะฟะพะถะฐะปัƒะนัั‚ะฐ, ะฟะพัะตั‚ะธั‚ะต ะฝะฐัˆ [Wiki](https://docs.gofiber.io/benchmarks). - -

- - -

- -## โš™๏ธ ะฃัั‚ะฐะฝะพะฒะบะฐ - -ะŸั€ะตะถะดะต ะฒัะตะณะพ, [ัะบะฐั‡ะฐะนั‚ะต](https://golang.org/dl/) ะธ ัƒัั‚ะฐะฝะพะฒะธั‚ะต Go. ะ’ะตั€ัะธั **1.11** ะธะปะธ ะฒั‹ัˆะต. - -ะฃัั‚ะฐะฝะพะฒะบะฐ ะฒั‹ะฟะพะปะฝัะตั‚ัั ั ะฟะพะผะพั‰ัŒัŽ ะบะพะผะฐะฝะดั‹ [`go get`](https://golang.org/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them): - -```bash -go get -u github.com/gofiber/fiber -``` - -## ๐ŸŽฏ ะžัะพะฑะตะฝะฝะพัั‚ะธ - -- ะะฐะดะตะถะฝะฐั [ะผะฐั€ัˆั€ัƒั‚ะธะทะฐั†ะธั](https://docs.gofiber.io/routing) -- ะ”ะพัั‚ัƒะฟ ะบ [ัั‚ะฐั‚ะธั‡ะฝั‹ะผ ั„ะฐะนะปะฐะผ](https://docs.gofiber.io/application#static) -- ะญะบัั‚ั€ะตะผะฐะปัŒะฝะฐั [ะฟั€ะพะธะทะฒะพะดะธั‚ะตะปัŒะฝะพัั‚ัŒ](https://docs.gofiber.io/benchmarks) -- [ะะธะทะบะธะน ะพะฑัŠะตะผ ะฟะพั‚ั€ะตะฑะปะตะฝะธั ะฟะฐะผัั‚ะธ](https://docs.gofiber.io/benchmarks) -- [ะญะฝะดะฟะพะธะฝั‚ั‹](https://docs.gofiber.io/context), ะบะฐะบ ะฒ [API](https://docs.gofiber.io/context) Express -- [Middleware](https://docs.gofiber.io/middleware) ะธ ะฟะพะดะดะตั€ะถะบะฐ [Next](https://docs.gofiber.io/context#next) -- [ะ‘ั‹ัั‚ั€ะพะต](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) ะฟั€ะพะณั€ะฐะผะผะธั€ะพะฒะฐะฝะธะต ะฝะฐ ัั‚ะพั€ะพะฝะต ัะตั€ะฒะตั€ะฐ -- [Template engines](https://github.com/gofiber/template) -- [ะŸะพะดะดะตั€ะถะบะฐ WebSocket](https://docs.gofiber.io/middleware#websocket) -- [Rate Limiter](https://docs.gofiber.io/middleware#limiter) -- ะ”ะพะบัƒะผะตะฝั‚ะฐั†ะธั ะดะพัั‚ัƒะฟะฝะฐ ะฝะฐ [15 ัะทั‹ะบะฐั…](https://docs.gofiber.io/) -- ะ˜ ะผะฝะพะณะพะต ะดั€ัƒะณะพะต, [ะฟะพัะตั‚ะธั‚ะต ะฝะฐัˆ Wiki](https://docs.gofiber.io/) - -## ๐Ÿ’ก ะคะธะปะพัะพั„ะธั - -ะะพะฒั‹ะต Go-ะฟั€ะพะณั€ะฐะผะผะธัั‚ั‹, ะบะพั‚ะพั€ั‹ะต ะฟะตั€ะตะบะปัŽั‡ะฐัŽั‚ัั ั [Node.js](https://nodejs.org/en/about/) ะฝะฐ [Go](https://golang.org/doc/), ะธะผะตัŽั‚ ะดะตะปะพ ั ะพั‡ะตะฝัŒ ะธะทะฒะธะปะธัั‚ะพะน ะบั€ะธะฒะพะน ะพะฑัƒั‡ะตะฝะธั, ะฟั€ะตะถะดะต ั‡ะตะผ ะพะฝะธ ัะผะพะณัƒั‚ ะฝะฐั‡ะฐั‚ัŒ ัะพะทะดะฐะฒะฐั‚ัŒ ัะฒะพะธ ะฒะตะฑ-ะฟั€ะธะปะพะถะตะฝะธั ะธะปะธ ะผะธะบั€ะพัะตั€ะฒะธัั‹. Fiber, ะบะฐะบ **ะฒะตะฑ-ั„ั€ะตะนะผะฒะพั€ะบ**, ะฑั‹ะป ัะพะทะดะฐะฝ ั ะธะดะตะตะน **ะผะธะฝะธะผะฐะปะธะทะผะฐ** ะธ ัะปะตะดะพะฒะฐะป **ะฟั€ะธะฝั†ะธะฟัƒ UNIX**, ั‚ะฐะบ ั‡ั‚ะพ ะฝะพะฒะธั‡ะบะธ ัะผะพะณัƒั‚ ะฑั‹ัั‚ั€ะพ ะฒะพะนั‚ะธ ะฒ ะผะธั€ Go ะฑะตะท ะพัะพะฑั‹ั… ะฟั€ะพะฑะปะตะผ. - -Fiber **ะฒะดะพั…ะฝะพะฒะปะตะฝ** Express, ัะฐะผั‹ะผ ะฟะพะฟัƒะปัั€ะฝั‹ะผ ะฒะตะฑ ั„ั€ะตะนะผะฒะพั€ะบะพะผ ะฒ ะ˜ะฝั‚ะตั€ะฝะตั‚ะต. ะœั‹ ะพะฑัŠะตะดะธะฝะธะปะธ **ะฟั€ะพัั‚ะพั‚ัƒ** Express ะธ **ั‡ะธัั‚ัƒัŽ ะฟั€ะพะธะทะฒะพะดะธั‚ะตะปัŒะฝะพัั‚ัŒ** Go. ะ•ัะปะธ ะฒั‹ ะบะพะณะดะฐ-ะปะธะฑะพ ั€ะตะฐะปะธะทะพะฒั‹ะฒะฐะปะธ ะฒะตะฑ-ะฟั€ะธะปะพะถะตะฝะธะต ะฝะฐ Node.js (*ั ะธัะฟะพะปัŒะทะพะฒะฐะฝะธะตะผ Express ะธะปะธ ะฐะฝะฐะปะพะณะธั‡ะฝะพะณะพ ั„ั€ะตะนะผะฒะพั€ะบะฐ*), ั‚ะพ ะผะฝะพะณะธะต ะผะตั‚ะพะดั‹ ะธ ะฟั€ะธะฝั†ะธะฟั‹ ะฟะพะบะฐะถัƒั‚ัั ะฒะฐะผ **ะพั‡ะตะฝัŒ ะทะฝะฐะบะพะผั‹ะผะธ**. - -ะœั‹ **ะฟั€ะธัะปัƒัˆะธะฒะฐะตะผัั** ะบ ะฝะฐัˆะธะผ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปัะผ ะฒ [issues](https://github.com/gofiber/fiber/issues), Discord [ะบะฐะฝะฐะปะต](https://gofiber.io/discord) _ะธ ะฒ ะพัั‚ะฐะปัŒะฝะพะผ ะ˜ะฝั‚ะตั€ะฝะตั‚ะต_, ั‡ั‚ะพะฑั‹ ัะพะทะดะฐั‚ัŒ **ะฑั‹ัั‚ั€ั‹ะน**, **ะณะธะฑะบะธะน** ะธ **ะดั€ัƒะถะตะปัŽะฑะฝั‹ะน** ะฒะตะฑ ั„ั€ะตะนะผะฒะพั€ะบ ะฝะฐ Go ะดะปั **ะปัŽะฑั‹ั…** ะทะฐะดะฐั‡, **ะดะตะดะปะฐะนะฝะพะฒ** ะธ **ัƒั€ะพะฒะฝะตะน** ั€ะฐะทั€ะฐะฑะพั‚ั‡ะธะบะพะฒ! ะšะฐะบ ัั‚ะพ ะดะตะปะฐะตั‚ Express ะฒ ะผะธั€ะต JavaScript. - -## ๐Ÿ‘€ ะŸั€ะธะผะตั€ั‹ - -ะะธะถะต ะฟะตั€ะตั‡ะธัะปะตะฝั‹ ะฝะตะบะพั‚ะพั€ั‹ะต ะธะท ั€ะฐัะฟั€ะพัั‚ั€ะฐะฝะตะฝะฝั‹ั… ะฟั€ะธะผะตั€ะพะฒ. ะ•ัะปะธ ะฒั‹ ั…ะพั‚ะธั‚ะต ัƒะฒะธะดะตั‚ัŒ ะฑะพะปัŒัˆะต ะฟั€ะธะผะตั€ะพะฒ ะบะพะดะฐ, ะฟะพะถะฐะปัƒะนัั‚ะฐ, ะฟะพัะตั‚ะธั‚ะต ะฝะฐัˆ [ั€ะตะฟะพะทะธั‚ะพั€ะธะน ั€ะตั†ะตะฟั‚ะพะฒ](https://github.com/gofiber/recipes) ะธะปะธ [ะดะพะบัƒะผะตะฝั‚ะฐั†ะธัŽ ะฟะพ API](https://docs.gofiber.io). - -Listed below are some of the common examples. If you want to see more code examples , please visit our [Recipes repository](https://github.com/gofiber/recipes) or visit our hosted [API documentation](https://docs.gofiber.io). - -#### ๐Ÿ“– [**Basic Routing**](https://docs.gofiber.io/#basic-routing) - -```go -func main() { - app := fiber.New() - - // GET /john - app.Get("/:name", func(c *fiber.Ctx) { - msg := fmt.Sprintf("Hello, %s ๐Ÿ‘‹!", c.Params("name")) - c.Send(msg) // => Hello john ๐Ÿ‘‹! - }) - - // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ‘ด %s is %s years old", c.Params("name"), c.Params("age")) - c.Send(msg) // => ๐Ÿ‘ด john is 75 years old - }) - - // GET /dictionary.txt - app.Get("/:file.:ext", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ“ƒ %s.%s", c.Params("file"), c.Params("ext")) - c.Send(msg) // => ๐Ÿ“ƒ dictionary.txt - }) - - // GET /flights/LAX-SFO - app.Get("/flights/:from-:to", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ’ธ From: %s, To: %s", c.Params("from"), c.Params("to")) - c.Send(msg) // => ๐Ÿ’ธ From: LAX, To: SFO - }) - - // GET /api/register - app.Get("/api/*", func(c *fiber.Ctx) { - msg := fmt.Sprintf("โœ‹ %s", c.Params("*")) - c.Send(msg) // => โœ‹ /api/register - }) - - app.Listen(3000) -} -``` - -#### ๐Ÿ“– [**Serving Static Files**](https://docs.gofiber.io/application#static) - -```go -func main() { - app := fiber.New() - - app.Static("/", "./public") - // => http://localhost:3000/js/script.js - // => http://localhost:3000/css/style.css - - app.Static("/prefix", "./public") - // => http://localhost:3000/prefix/js/script.js - // => http://localhost:3000/prefix/css/style.css - - app.Static("*", "./public/index.html") - // => http://localhost:3000/any/path/shows/index/html - - app.Listen(3000) -} -``` - -#### ๐Ÿ“– [**Middleware & Next**](https://docs.gofiber.io/context#next) - -```go -func main() { - app := fiber.New() - - // Match any route - app.Use(func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‡ First handler") - c.Next() - }) - - // Match all routes starting with /api - app.Use("/api", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅˆ Second handler") - c.Next() - }) - - // GET /api/register - app.Get("/api/list", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‰ Last handler") - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -
- ๐Ÿ“š ะŸะพะบะฐะทะฐั‚ัŒ ะฑะพะปัŒัˆะต ะฟั€ะธะผะตั€ะพะฒ ะบะพะดะฐ - -### Views engines - -๐Ÿ“– [Settings](https://docs.gofiber.io/application#settings) -๐Ÿ“– [Engines](https://github.com/gofiber/template) -๐Ÿ“– [Render](https://docs.gofiber.io/context#render) - -Fiber defaults to the [html/template](https://golang.org/pkg/html/template/) when no view engine is set. - -If you want to execute partials or use a different engine like [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) or [pug](https://github.com/Joker/jade) etc.. - -Checkout our [Template](https://github.com/gofiber/template) package that support multiple view engines. - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/template/pug" -) - -func main() { - // You can setup Views engine before initiation app: - app := fiber.New(&fiber.Settings{ - Views: pug.New("./views", ".pug"), - }) - - // OR after initiation app at any convenient location: - app.Settings.Views = pug.New("./views", ".pug"), - - // And now, you can call template `./views/home.pug` like this: - app.Get("/", func(c *fiber.Ctx) { - c.Render("home", fiber.Map{ - "title": "Homepage", - "year": 1999, - }) - }) - - // ... -} -``` - -### Grouping routes into chains - -๐Ÿ“– [Group](https://docs.gofiber.io/application#group) - -```go -func main() { - app := fiber.New() - - // Root API route - api := app.Group("/api", cors()) // /api - - // API v1 routes - v1 := api.Group("/v1", mysql()) // /api/v1 - v1.Get("/list", handler) // /api/v1/list - v1.Get("/user", handler) // /api/v1/user - - // API v2 routes - v2 := api.Group("/v2", mongodb()) // /api/v2 - v2.Get("/list", handler) // /api/v2/list - v2.Get("/user", handler) // /api/v2/user - - // ... -} -``` - -### Middleware logger - -๐Ÿ“– [Logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - // Default - app.Use(middleware.Logger()) - - // Custom logging format - app.Use(middleware.Logger("${method} - ${path}")) - - // Custom Config - app.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ - Next: func(ctx *fiber.Ctx) bool { - return ctx.Path() != "/private" - }, - Format: "${method} - ${path}", - Output: io.Writer, - })) - - app.Listen(3000) -} -``` - -### Cross-Origin Resource Sharing (CORS) - -๐Ÿ“– [CORS](https://docs.gofiber.io/middleware#cors) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/cors" -) - -func main() { - app := fiber.New() - - // CORS with default config - app.Use(cors.New()) - - app.Listen(3000) -} -``` - -ะŸั€ะพะฒะตั€ะตะผ CORS, ะฟั€ะธัะฒะพะธะฒ ะดะพะผะตะฝ ะฒ ะทะฐะณะพะปะพะฒะพะบ `Origin`, ะพั‚ะปะธั‡ะฝั‹ะน ะพั‚ `localhost`: - -```bash -curl -H "Origin: http://example.com" --verbose http://localhost:3000 -``` - -### Custom 404 response - -๐Ÿ“– [HTTP Methods](https://docs.gofiber.io/application#http-methods) - -```go -func main() { - app := fiber.New() - - app.Static("./public") - - app.Get("/demo", func(c *fiber.Ctx) { - c.Send("This is a demo!") - }) - - app.Post("/register", func(c *fiber.Ctx) { - c.Send("Welcome!") - }) - - // Last middleware to match anything - app.Use(func(c *fiber.Ctx) { - c.SendStatus(404) - // => 404 "Not Found" - }) - - app.Listen(3000) -} -``` - -### JSON Response - -๐Ÿ“– [JSON](https://docs.gofiber.io/context#json) - -```go -type User struct { - Name string `json:"name"` - Age int `json:"age"` -} - -func main() { - app := fiber.New() - - app.Get("/user", func(c *fiber.Ctx) { - c.JSON(&User{"John", 20}) - // => {"name":"John", "age":20} - }) - - app.Get("/json", func(c *fiber.Ctx) { - c.JSON(fiber.Map{ - "success": true, - "message": "Hi John!", - }) - // => {"success":true, "message":"Hi John!"} - }) - - app.Listen(3000) -} -``` - -### WebSocket Upgrade - -๐Ÿ“– [Websocket](https://docs.gofiber.io/middleware#websocket) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/websocket" -) - -func main() { - app := fiber.New() - - app.Get("/ws", websocket.New(func(c *websocket.Conn) { - for { - mt, msg, err := c.ReadMessage() - if err != nil { - log.Println("read:", err) - break - } - log.Printf("recv: %s", msg) - err = c.WriteMessage(mt, msg) - if err != nil { - log.Println("write:", err) - break - } - } - })) - - app.Listen(3000) - // ws://localhost:3000/ws -} -``` - -### Recover middleware - -๐Ÿ“– [Recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - app.Use(middleware.Recover()) - - app.Get("/", func(c *fiber.Ctx) { - panic("normally this would crash your app") - }) - - app.Listen(3000) -} -``` -
- -## ๐Ÿงฌ Fiber Middleware - -The Fiber middleware modules listed here are maintained by the [Fiber team](https://github.com/orgs/gofiber/people). - -| Middleware | Description | Built-in middleware | -| :--- | :--- | :--- | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | - | -| [basicauth](https://github.com/gofiber/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | - | -| [compress](https://github.com/gofiber/fiber/blob/master/middleware/compress.md) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | `middleware.Compress()` | -| [cors](https://github.com/gofiber/cors) | Enable cross-origin resource sharing \(CORS\) with various options. | - | -| [csrf](https://github.com/gofiber/csrf) | Protect from CSRF exploits. | - | -| [filesystem](https://github.com/gofiber/fiber/blob/master/middleware/filesystem.md) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | - | -| [favicon](https://github.com/gofiber/fiber/blob/master/middleware/favicon.md) | Ignore favicon from logs or serve from memory if a file path is provided. | `middleware.Favicon()` | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | - | -| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | - | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | - | -| [limiter](https://github.com/gofiber/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | - | -| [logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) | HTTP request/response logger. | `middleware.Logger()` | -| [pprof](https://github.com/gofiber/pprof) | Special thanks to Matthew Lee \(@mthli\) | - | -| [recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) | Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized[ ErrorHandler](error-handling.md). | `middleware.Recover()` | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | - | -| [requestid](https://github.com/gofiber/fiber/blob/master/middleware/request_id.md) | Request ID middleware generates a unique id for a request. | `middleware.RequestID()` | -| [session](https://github.com/gofiber/session) | This session middleware is build on top of fasthttp/session by @savsgio MIT. Special thanks to @thomasvvugt for helping with this middleware. | - | -| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | - | -| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | - | - -## ๐ŸŒฑ Middlewares ะพั‚ ัั‚ะพั€ะพะฝะฝะธั… ั€ะฐะทั€ะฐะฑะพั‚ั‡ะธะบะพะฒ - -ะญั‚ะพ ัะฟะธัะพะบ middlewares, ัะพะทะดะฐะฝะฝั‹ั… ัะพะพะฑั‰ะตัั‚ะฒะพะผ Fiber. ะŸะพะถะฐะปัƒะนัั‚ะฐ, [ัะพะทะดะฐะนั‚ะต PR](https://github.com/gofiber/fiber/pulls), ะตัะปะธ ั…ะพั‚ะธั‚ะต ะดะพะฑะฐะฒะธั‚ัŒ ะฒ ัั‚ะพั‚ ัะฟะธัะพะบ ัะฒะพะน ะธะปะธ ะธะทะฒะตัั‚ะฝั‹ะน ะฒะฐะผ middleware ะดะปั ะฒะตะฑ ั„ั€ะตะนะผะฒะพั€ะบะฐ Fiber! - -- [arsmn/fiber-casbin](https://github.com/arsmn/fiber-casbin) -- [arsmn/fiber-introspect](https://github.com/arsmn/fiber-introspect) -- [arsmn/fiber-swagger](https://github.com/arsmn/fiber-swagger) -- [arsmn/gqlgen](https://github.com/arsmn/gqlgen) -- [codemicro/fiber-cache](https://github.com/codemicro/fiber-cache) -- [itsursujit/fiber-boilerplate](https://github.com/itsursujit/fiber-boilerplate) -- [juandiii/go-jwk-security](https://github.com/juandiii/go-jwk-security) -- [kiyonlin/fiber_limiter](https://github.com/kiyonlin/fiber_limiter) -- [shareed2k/fiber_limiter](https://github.com/shareed2k/fiber_limiter) -- [shareed2k/fiber_tracing](https://github.com/shareed2k/fiber_tracing) -- [thomasvvugt/fiber-boilerplate](https://github.com/thomasvvugt/fiber-boilerplate) - -## ๐Ÿ’ฌ ะœะตะดะธะฐ - -

- - -

- -- [Welcome to Fiber โ€” an Express.js styled web framework written in Go with โค๏ธ](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) โ€” _03 Feb 2020_ -- [Fiber released v1.7! ๐ŸŽ‰ What's new and is it still fast, flexible and friendly?](https://dev.to/koddr/fiber-v2-is-out-now-what-s-new-and-is-he-still-fast-flexible-and-friendly-3ipf) โ€” _21 Feb 2020_ -- [๐Ÿš€ Fiber v1.8. What's new, updated and re-thinked?](https://dev.to/koddr/fiber-v1-8-what-s-new-updated-and-re-thinked-339h) โ€” _03 Mar 2020_ -- [Is switching from Express to Fiber worth it? ๐Ÿค”](https://dev.to/koddr/are-sure-what-your-lovely-web-framework-running-so-fast-2jl1) โ€” _01 Apr 2020_ -- [Creating Fast APIs In Go Using Fiber](https://dev.to/jozsefsallai/creating-fast-apis-in-go-using-fiber-59m9) โ€” _07 Apr 2020_ -- [Building a Basic REST API in Go using Fiber](https://tutorialedge.net/golang/basic-rest-api-go-fiber/) - _23 Apr 2020_ -- [๐Ÿ“บ Building a REST API using GORM and Fiber](https://youtu.be/Iq2qT0fRhAA) - _25 Apr 2020_ -- [๐ŸŒŽ Create a travel list app with Go, Fiber, Angular, MongoDB and Google Cloud Secret Manager](https://blog.yongweilun.me/create-a-travel-list-app-with-go-fiber-angular-mongodb-and-google-cloud-secret-manager-ck9fgxy0p061pcss1xt1ubu8t) - _25 Apr 2020_ -- [Fiber v1.9.6 ๐Ÿ”ฅ How to improve performance by 817% and stay fast, flexible and friendly?](https://dev.to/koddr/fiber-v1-9-5-how-to-improve-performance-by-817-and-stay-fast-flexible-and-friendly-2dp6) - _12 May 2020_ -- [The road to web-based authentication with Fiber โšก](https://vugt.me/the-road-to-web-based-authentication-with-fiber/) - _20 May 2020_ -- [Building an Express-style API in Go with Fiber](https://blog.logrocket.com/express-style-api-go-fiber/) - _10 June 2020_ -- [ๅŸบไบŽgolang fiberๅ’Œangularๅผ€ๅ‘web](https://zhuanlan.zhihu.com/p/148925642) - _19 June 2020_ -- [ๅŸบไบŽๅปถ่ฟŸ่ฎก็ฎ—ไปค็‰Œๆกถ็š„gofiber้ข‘็Ž‡้™ๅˆถไธญ้—ดไปถๅฎž็Žฐ](https://zhuanlan.zhihu.com/p/149308936) - _20 June 2020_ -- [Construir una API en Golang con Fiber ๐Ÿ‡ช๐Ÿ‡ธ](https://enbonnet.me/article/53/construir-api-golang-con-fiber) - _28 June 2020_ -- [๐Ÿ“บWhy Go Fiber Is THE New Framework To Learn](https://www.youtube.com/watch?v=kvwsPeWDLM8) - _29 June 2020_ -- [่งฃๆžGofiber่ทฏ็”ฑ็ฎก็†](https://zhuanlan.zhihu.com/p/152494502) - _08 July 2020_ -- [๐Ÿ“บ Introduction to Fiber - An Express-inspired web framework](https://youtu.be/MfFi4Gt-tos) - _25 July 2020_ - -## ๐Ÿ‘ ะŸะพะผะพั‰ัŒ ะฟั€ะพะตะบั‚ัƒ - -ะ•ัะปะธ ะฒั‹ ั…ะพั‚ะธั‚ะต ัะบะฐะทะฐั‚ัŒ **ัะฟะฐัะธะฑะพ** ะธ/ะธะปะธ ะฟะพะดะดะตั€ะถะฐั‚ัŒ ะฐะบั‚ะธะฒะฝะพะต ั€ะฐะทะฒะธั‚ะธะต `Fiber`: - -1. ะ”ะพะฑะฐะฒัŒั‚ะต [GitHub Star](https://github.com/gofiber/fiber/stargazers) ะฒ ะฟั€ะพะตะบั‚. -2. ะะฐะฟะธัˆะธั‚ะต ะพ ะฟั€ะพะตะบั‚ะต [ะฒ ะฒะฐัˆะตะผ Twitter](https://twitter.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). -3. ะกะดะตะปะฐะนั‚ะต ะพะฑะทะพั€ ั„ั€ะตะนะผะฒะพั€ะบะฐ ะฝะฐ [Medium](https://medium.com/), [Dev.to](https://dev.to/) ะธะปะธ ะฒ ะปะธั‡ะฝะพะผ ะฑะปะพะณะต. -4. ะŸะพะผะพะณะธั‚ะต ะฟะตั€ะตะฒะตัั‚ะธ ะฝะฐัˆัƒ API ะดะพะบัƒะผะตะฝั‚ะฐั†ะธัŽ ะฝะฐ ะฟะปะฐั‚ั„ะพั€ะผะต [Crowdin](https://crowdin.com/project/gofiber) [![Crowdin](https://badges.crowdin.net/gofiber/localized.svg)](https://crowdin.com/project/gofiber) -5. ะŸะพะดะดะตั€ะถะธั‚ะต ะฟั€ะพะตะบั‚, ะบัƒะฟะธะฒ [ั‡ะฐัˆะบัƒ ะบะพั„ะต](https://buymeacoff.ee/fenny). - -## โ˜• ะŸะพะดะดะตั€ะถะบะฐ ะฟั€ะพะตะบั‚ะฐ - -Fiber โ€” ัั‚ะพ ะฟั€ะพะตะบั‚ ั ะพั‚ะบั€ั‹ั‚ั‹ะผ ะธัั…ะพะดะฝั‹ะผ ะบะพะดะพะผ, ะบะพั‚ะพั€ั‹ะน ั€ะฐะฑะพั‚ะฐะตั‚ ะฝะฐ ะฟะพะถะตั€ั‚ะฒะพะฒะฐะฝะธั ะดะปั ะพะฟะปะฐั‚ั‹ ัั‡ะตั‚ะพะฒ, ะฝะฐะฟั€ะธะผะตั€, ะฝะฐัˆะตะณะพ ะดะพะผะตะฝะฝะพะณะพ ะธะผะตะฝะธ, GitBook, Netlify ะธ serverless-ั…ะพัั‚ะธะฝะณะฐ. - -ะ•ัะปะธ ะฒั‹ ั…ะพั‚ะธั‚ะต ะฟะพะดะดะตั€ะถะฐั‚ัŒ, ั‚ะพ โ˜• [**ะบัƒะฟะธั‚ะต ั‡ะฐัˆะบัƒ ะบะพั„ะต**](https://buymeacoff.ee/fenny). - -| | ะŸะพะปัŒะทะพะฒะฐั‚ะตะปะธ | ะŸะพะถะตั€ั‚ะฒะพะฒะฐะฝะธั | -| :---------------------------------------------------------- | :----------------------------------------------- | :-------- | -| ![](https://avatars.githubusercontent.com/u/204341?s=25 ) | [@destari](https://github.com/destari) | โ˜• x 10 | -| ![](https://avatars.githubusercontent.com/u/63164982?s=25 ) | [@dembygenesis](https://github.com/dembygenesis) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/56607882?s=25 ) | [@thomasvvugt](https://github.com/thomasvvugt) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/27820675?s=25 ) | [@hendratommy](https://github.com/hendratommy) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/1094221?s=25 ) | [@ekaputra07](https://github.com/ekaputra07) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/194590?s=25 ) | [@jorgefuertes](https://github.com/jorgefuertes) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/186637?s=25 ) | [@candidosales](https://github.com/candidosales) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/29659953?s=25 ) | [@l0nax](https://github.com/l0nax) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/59947262?s=25 ) | [@ankush](https://github.com/ankush) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/635852?s=25 ) | [@bihe](https://github.com/bihe) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/307334?s=25 ) | [@justdave](https://github.com/justdave) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/11155743?s=25 ) | [@koddr](https://github.com/koddr) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/29042462?s=25 ) | [@lapolinar](https://github.com/lapolinar) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/2978730?s=25 ) | [@diegowifi](https://github.com/diegowifi) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/44171355?s=25 ) | [@ssimk0](https://github.com/ssimk0) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/5638101?s=25 ) | [@raymayemir](https://github.com/raymayemir) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/619996?s=25 ) | [@melkorm](https://github.com/melkorm) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31022056?s=25 ) | [@marvinjwendt](https://github.com/thomasvvugt) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31921460?s=25 ) | [@toishy](https://github.com/toishy) | โ˜• x 1 | - -## โ€Žโ€๐Ÿ’ป ะšะพะฝั‚ั€ะธะฑัŒัŽั‚ะตั€ั‹ - -Code Contributors - -## โญ๏ธ Stargazers - -Stargazers over time - -## โš ๏ธ License - -Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` is free and open-source software licensed under the [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Official logo was created by [Vic Shรณstak](https://github.com/koddr) and distributed under [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) license (CC BY-SA 4.0 International). - -**Third-party library licenses** -- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) -- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) -- [fasttemplate](https://github.com/valyala/fasttemplate/blob/master/LICENSE) -- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) diff --git a/.github/README_tr.md b/.github/README_tr.md deleted file mode 100644 index 5e5ff3e3..00000000 --- a/.github/README_tr.md +++ /dev/null @@ -1,588 +0,0 @@ -

- - Fiber - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -

-

- Fiber, Go iรงin en hฤฑzlฤฑ HTTP motoru olan Fasthttp รผzerine inลŸa edilmiลŸ, Express den ilham alan bir web รงatฤฑsฤฑdฤฑr. Sฤฑfฤฑr bellek ayฤฑrma ve performans gรถz รถnรผnde bulundurularak hฤฑzlฤฑ geliลŸtirme iรงin iลŸleri kolaylaลŸtฤฑrmak รผzere tasarlandฤฑ. -

- -## โšก๏ธ Hฤฑzlฤฑ BaลŸlangฤฑรง - -```go -package main - -import "github.com/gofiber/fiber" - -func main() { - app := fiber.New() - - app.Get("/", func(c *fiber.Ctx) { - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -## ๐Ÿค– Performans ร–lรงรผmleri - -Bu testler [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) ve [Go Web](https://github.com/smallnest/go-web-framework-benchmark) ile koลŸuldu. Bรผtรผn sonuรงlarฤฑ gรถrmek iรงin lรผtfen [Wiki](https://docs.gofiber.io/benchmarks) sayfasฤฑnฤฑ ziyaret ediniz. - -

- - -

- -## โš™๏ธ Kurulum - -ฤฐlk รถnce, Go yu [indirip](https://golang.org/dl/) kuruyoruz. `1.11` veya daha yeni sรผrรผm gereklidir. - -[`go get`](https://golang.org/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) komutunu kullanarak kurulumu tamamlฤฑyoruz: - -```bash -go get -u github.com/gofiber/fiber/... -``` - -## ๐ŸŽฏ ร–zellikler - -- Gรผรงlรผ [rotalar](https://docs.gofiber.io/routing) -- [Statik dosya](https://docs.gofiber.io/application#static) yรถnetimi -- OlaฤŸanรผstรผ [performans](https://docs.gofiber.io/benchmarks) -- [DรผลŸรผk bellek](https://docs.gofiber.io/benchmarks) tรผketimi -- [API uรง noktalarฤฑ](https://docs.gofiber.io/context) -- Ara katman & [Sonraki](https://docs.gofiber.io/context#next) desteฤŸi -- [Hฤฑzlฤฑ](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) sunucu taraflฤฑ programlama -- [Template engines](https://github.com/gofiber/template) -- [WebSocket support](https://docs.gofiber.io/middleware#websocket) -- [Rate Limiter](https://docs.gofiber.io/middleware#limiter) -- Available in [15 languages](https://docs.gofiber.io/) -- Ve daha fazlasฤฑ, [Fiber ฤฑ keลŸfet](https://docs.gofiber.io/) - -## ๐Ÿ’ก Felsefe - -[Node.js](https://nodejs.org/en/about/) den [Go](https://golang.org/doc/) ya geรงen yeni gopher lar kendi web uygulamalarฤฑnฤฑ ve mikroservislerini yazmaya baลŸlamadan รถnce dili รถฤŸrenmek ile uฤŸraลŸฤฑyorlar. Fiber, bir **web รงatฤฑsฤฑ** olarak, **minimalizm** ve **UNIX yolu**nu izlemek fikri ile oluลŸturuldu. Bรถylece yeni gopher lar sฤฑcak ve gรผvenilir bir hoลŸgeldin ile Go dรผnyasฤฑna giriลŸ yapabilirler. - -Fiber internet รผzerinde en popรผler olan Express web รงatฤฑsฤฑndan **esinlenmiลŸtir**. Biz Express in **kolaylฤฑฤŸฤฑnฤฑ** ve Go nun **ham performansฤฑnฤฑ** birleลŸtirdik. Daha รถnce Node.js รผzerinde (Express veya benzerini kullanarak) bir web uygulamasฤฑ geliลŸtirdiyseniz, pek รงok metod ve prensip size **รงok tanฤฑdฤฑk** gelecektir. - -## ๐Ÿ‘€ ร–rnekler - -AลŸaฤŸฤฑda yaygฤฑn รถrneklerden bazฤฑlarฤฑ listelenmiลŸtir. Daha fazla kod รถrneฤŸi gรถrmek iรงin, lรผtfen [Kod deposunu](https://github.com/gofiber/recipes) veya [API dรถkรผmantasyonunu](https://docs.gofiber.io) ziyaret ediniz. - -#### ๐Ÿ“– [**Basic Routing**](https://docs.gofiber.io/#basic-routing) - -```go -func main() { - app := fiber.New() - - // GET /john - app.Get("/:name", func(c *fiber.Ctx) { - msg := fmt.Sprintf("Hello, %s ๐Ÿ‘‹!", c.Params("name")) - c.Send(msg) // => Hello john ๐Ÿ‘‹! - }) - - // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ‘ด %s is %s years old", c.Params("name"), c.Params("age")) - c.Send(msg) // => ๐Ÿ‘ด john is 75 years old - }) - - // GET /dictionary.txt - app.Get("/:file.:ext", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ“ƒ %s.%s", c.Params("file"), c.Params("ext")) - c.Send(msg) // => ๐Ÿ“ƒ dictionary.txt - }) - - // GET /flights/LAX-SFO - app.Get("/flights/:from-:to", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ’ธ From: %s, To: %s", c.Params("from"), c.Params("to")) - c.Send(msg) // => ๐Ÿ’ธ From: LAX, To: SFO - }) - - // GET /api/register - app.Get("/api/*", func(c *fiber.Ctx) { - msg := fmt.Sprintf("โœ‹ %s", c.Params("*")) - c.Send(msg) // => โœ‹ /api/register - }) - - app.Listen(3000) -} -``` - -#### ๐Ÿ“– [**Serving Static Files**](https://docs.gofiber.io/application#static) - -```go -func main() { - app := fiber.New() - - app.Static("/", "./public") - // => http://localhost:3000/js/script.js - // => http://localhost:3000/css/style.css - - app.Static("/prefix", "./public") - // => http://localhost:3000/prefix/js/script.js - // => http://localhost:3000/prefix/css/style.css - - app.Static("*", "./public/index.html") - // => http://localhost:3000/any/path/shows/index/html - - app.Listen(3000) -} -``` - -#### ๐Ÿ“– [**Middleware & Next**](https://docs.gofiber.io/context#next) - -```go -func main() { - app := fiber.New() - - // Match any route - app.Use(func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‡ First handler") - c.Next() - }) - - // Match all routes starting with /api - app.Use("/api", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅˆ Second handler") - c.Next() - }) - - // GET /api/register - app.Get("/api/list", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‰ Last handler") - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -
- ๐Ÿ“š Daha fazla kod รถrneฤŸi gรถster - -### Views engines - -๐Ÿ“– [Settings](https://docs.gofiber.io/application#settings) -๐Ÿ“– [Engines](https://github.com/gofiber/template) -๐Ÿ“– [Render](https://docs.gofiber.io/context#render) - -Fiber defaults to the [html/template](https://golang.org/pkg/html/template/) when no view engine is set. - -If you want to execute partials or use a different engine like [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) or [pug](https://github.com/Joker/jade) etc.. - -Checkout our [Template](https://github.com/gofiber/template) package that support multiple view engines. - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/template/pug" -) - -func main() { - // You can setup Views engine before initiation app: - app := fiber.New(&fiber.Settings{ - Views: pug.New("./views", ".pug"), - }) - - // OR after initiation app at any convenient location: - app.Settings.Views = pug.New("./views", ".pug"), - - // And now, you can call template `./views/home.pug` like this: - app.Get("/", func(c *fiber.Ctx) { - c.Render("home", fiber.Map{ - "title": "Homepage", - "year": 1999, - }) - }) - - // ... -} -``` - -### Rotalarฤฑ Zincirlere Gruplama - -๐Ÿ“– [Grup](https://docs.gofiber.io/application#group) - -```go -func main() { - app := fiber.New() - - // Kรถk API rotasฤฑ - api := app.Group("/api", cors()) // /api - - // API v1 rotasฤฑ - v1 := api.Group("/v1", mysql()) // /api/v1 - v1.Get("/list", handler) // /api/v1/list - v1.Get("/user", handler) // /api/v1/user - - // API v2 rotasฤฑ - v2 := api.Group("/v2", mongodb()) // /api/v2 - v2.Get("/list", handler) // /api/v2/list - v2.Get("/user", handler) // /api/v2/user - - // ... -} -``` - -### Ara Katman Gรผnlรผkcรผsรผ(Logger) - -๐Ÿ“– [Gรผnlรผkcรผ](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - // Default - app.Use(middleware.Logger()) - - // Custom logging format - app.Use(middleware.Logger("${method} - ${path}")) - - // Custom Config - app.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ - Next: func(ctx *fiber.Ctx) bool { - return ctx.Path() != "/private" - }, - Format: "${method} - ${path}", - Output: io.Writer, - })) - - app.Listen(3000) -} -``` - -### Farklฤฑ Merkezler Arasฤฑ Kaynak PaylaลŸฤฑmฤฑ (CORS) - -๐Ÿ“– [CORS](https://docs.gofiber.io/middleware#cors) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/cors" -) - -func main() { - app := fiber.New() - - // Varsayฤฑlan ayarlarla CORS - app.Use(cors.New()) - - app.Listen(3000) -} -``` - -`Origin` baลŸlฤฑฤŸฤฑ iรงinde herhangฤฑ bir alan adฤฑ kullanarak CORS'u kontrol et: - -```bash -curl -H "Origin: http://example.com" --verbose http://localhost:3000 -``` - -### ร–zelleลŸtirilebilir 404 yanฤฑtlarฤฑ - -๐Ÿ“– [HTTP Methodlari](https://docs.gofiber.io/application#http-methods) - -```go -func main() { - app := fiber.New() - - app.Static("./public") - - app.Get("/demo", func(c *fiber.Ctx) { - c.Send("This is a demo!") - }) - - app.Post("/register", func(c *fiber.Ctx) { - c.Send("Welcome!") - }) - - // Herhangi bir ลŸeyle eลŸleลŸen son ara katman - app.Use(func(c *fiber.Ctx) { - c.SendStatus(404) - // => 404 "Not Found" - }) - - app.Listen(3000) -} -``` - -### JSON Yanฤฑtlarฤฑ - -๐Ÿ“– [JSON](https://docs.gofiber.io/context#json) - -```go -type User struct { - Name string `json:"name"` - Age int `json:"age"` -} - -func main() { - app := fiber.New() - - app.Get("/user", func(c *fiber.Ctx) { - c.JSON(&User{"John", 20}) - // => {"name":"John", "age":20} - }) - - app.Get("/json", func(c *fiber.Ctx) { - c.JSON(fiber.Map{ - "success": true, - "message": "Hi John!", - }) - // => {"success":true, "message":"Hi John!"} - }) - - app.Listen(3000) -} -``` - -### WebSocket Yรผkseltmesi - -๐Ÿ“– [Websocket](https://docs.gofiber.io/middleware#websocket) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/websocket" -) - -func main() { - app := fiber.New() - - app.Get("/ws", websocket.New(func(c *websocket.Conn) { - for { - mt, msg, err := c.ReadMessage() - if err != nil { - log.Println("read:", err) - break - } - log.Printf("recv: %s", msg) - err = c.WriteMessage(mt, msg) - if err != nil { - log.Println("write:", err) - break - } - } - })) - - app.Listen(3000) - // ws://localhost:3000/ws -} -``` - -### Recover middleware - -๐Ÿ“– [Recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - app.Use(middleware.Recover()) - - app.Get("/", func(c *fiber.Ctx) { - panic("normally this would crash your app") - }) - - app.Listen(3000) -} -``` -
- -## ๐Ÿงฌ Fiber Middleware - -The Fiber middleware modules listed here are maintained by the [Fiber team](https://github.com/orgs/gofiber/people). - -| Middleware | Description | Built-in middleware | -| :--- | :--- | :--- | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | - | -| [basicauth](https://github.com/gofiber/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | - | -| [compress](https://github.com/gofiber/fiber/blob/master/middleware/compress.md) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | `middleware.Compress()` | -| [cors](https://github.com/gofiber/cors) | Enable cross-origin resource sharing \(CORS\) with various options. | - | -| [csrf](https://github.com/gofiber/csrf) | Protect from CSRF exploits. | - | -| [filesystem](https://github.com/gofiber/fiber/blob/master/middleware/filesystem.md) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | - | -| [favicon](https://github.com/gofiber/fiber/blob/master/middleware/favicon.md) | Ignore favicon from logs or serve from memory if a file path is provided. | `middleware.Favicon()` | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | - | -| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | - | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | - | -| [limiter](https://github.com/gofiber/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | - | -| [logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) | HTTP request/response logger. | `middleware.Logger()` | -| [pprof](https://github.com/gofiber/pprof) | Special thanks to Matthew Lee \(@mthli\) | - | -| [recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) | Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized[ ErrorHandler](error-handling.md). | `middleware.Recover()` | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | - | -| [requestid](https://github.com/gofiber/fiber/blob/master/middleware/request_id.md) | Request ID middleware generates a unique id for a request. | `middleware.RequestID()` | -| [session](https://github.com/gofiber/session) | This session middleware is build on top of fasthttp/session by @savsgio MIT. Special thanks to @thomasvvugt for helping with this middleware. | - | -| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | - | -| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | - | - -## ๐ŸŒฑ Third Party Middlewares - -This is a list of middlewares that are created by the Fiber community, please create a PR if you want to see yours! -- [arsmn/fiber-casbin](https://github.com/arsmn/fiber-casbin) -- [arsmn/fiber-introspect](https://github.com/arsmn/fiber-introspect) -- [arsmn/fiber-swagger](https://github.com/arsmn/fiber-swagger) -- [arsmn/gqlgen](https://github.com/arsmn/gqlgen) -- [codemicro/fiber-cache](https://github.com/codemicro/fiber-cache) -- [itsursujit/fiber-boilerplate](https://github.com/itsursujit/fiber-boilerplate) -- [juandiii/go-jwk-security](https://github.com/juandiii/go-jwk-security) -- [kiyonlin/fiber_limiter](https://github.com/kiyonlin/fiber_limiter) -- [shareed2k/fiber_limiter](https://github.com/shareed2k/fiber_limiter) -- [shareed2k/fiber_tracing](https://github.com/shareed2k/fiber_tracing) -- [thomasvvugt/fiber-boilerplate](https://github.com/thomasvvugt/fiber-boilerplate) - -## ๐Ÿ’ฌ Medya - -

- - -

- -- [Welcome to Fiber โ€” an Express.js styled web framework written in Go with โค๏ธ](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) โ€” _03 Feb 2020_ -- [Fiber released v1.7! ๐ŸŽ‰ What's new and is it still fast, flexible and friendly?](https://dev.to/koddr/fiber-v2-is-out-now-what-s-new-and-is-he-still-fast-flexible-and-friendly-3ipf) โ€” _21 Feb 2020_ -- [๐Ÿš€ Fiber v1.8. What's new, updated and re-thinked?](https://dev.to/koddr/fiber-v1-8-what-s-new-updated-and-re-thinked-339h) โ€” _03 Mar 2020_ -- [Is switching from Express to Fiber worth it? ๐Ÿค”](https://dev.to/koddr/are-sure-what-your-lovely-web-framework-running-so-fast-2jl1) โ€” _01 Apr 2020_ -- [Creating Fast APIs In Go Using Fiber](https://dev.to/jozsefsallai/creating-fast-apis-in-go-using-fiber-59m9) โ€” _07 Apr 2020_ -- [Building a Basic REST API in Go using Fiber](https://tutorialedge.net/golang/basic-rest-api-go-fiber/) - _23 Apr 2020_ -- [๐Ÿ“บ Building a REST API using GORM and Fiber](https://youtu.be/Iq2qT0fRhAA) - _25 Apr 2020_ -- [๐ŸŒŽ Create a travel list app with Go, Fiber, Angular, MongoDB and Google Cloud Secret Manager](https://blog.yongweilun.me/create-a-travel-list-app-with-go-fiber-angular-mongodb-and-google-cloud-secret-manager-ck9fgxy0p061pcss1xt1ubu8t) - _25 Apr 2020_ -- [Fiber v1.9.6 ๐Ÿ”ฅ How to improve performance by 817% and stay fast, flexible and friendly?](https://dev.to/koddr/fiber-v1-9-5-how-to-improve-performance-by-817-and-stay-fast-flexible-and-friendly-2dp6) - _12 May 2020_ -- [The road to web-based authentication with Fiber โšก](https://vugt.me/the-road-to-web-based-authentication-with-fiber/) - _20 May 2020_ -- [Building an Express-style API in Go with Fiber](https://blog.logrocket.com/express-style-api-go-fiber/) - _10 June 2020_ -- [ๅŸบไบŽgolang fiberๅ’Œangularๅผ€ๅ‘web](https://zhuanlan.zhihu.com/p/148925642) - _19 June 2020_ -- [ๅŸบไบŽๅปถ่ฟŸ่ฎก็ฎ—ไปค็‰Œๆกถ็š„gofiber้ข‘็Ž‡้™ๅˆถไธญ้—ดไปถๅฎž็Žฐ](https://zhuanlan.zhihu.com/p/149308936) - _20 June 2020_ -- [Construir una API en Golang con Fiber ๐Ÿ‡ช๐Ÿ‡ธ](https://enbonnet.me/article/53/construir-api-golang-con-fiber) - _28 June 2020_ -- [๐Ÿ“บWhy Go Fiber Is THE New Framework To Learn](https://www.youtube.com/watch?v=kvwsPeWDLM8) - _29 June 2020_ -- [่งฃๆžGofiber่ทฏ็”ฑ็ฎก็†](https://zhuanlan.zhihu.com/p/152494502) - _08 July 2020_ -- [๐Ÿ“บ Introduction to Fiber - An Express-inspired web framework](https://youtu.be/MfFi4Gt-tos) - _25 July 2020_ - -## ๐Ÿ‘ Destek - -EฤŸer **teลŸekkรผr etmek** ve/veya `Fiber`'in aktif geliลŸtirilmesini desteklemek istiyorsanฤฑz: - -1. Projeye [GitHub Yฤฑldฤฑzฤฑ](https://github.com/gofiber/fiber/stargazers) verin. -2. [Twitter hesabฤฑnฤฑzdan](https://twitter.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber) proje hakkฤฑnda tweet atฤฑn. -3. [Medium](https://medium.com/), [Dev.to](https://dev.to/) veya kiลŸisel blog รผzerinden bir inceleme veya eฤŸitici yazฤฑ yazฤฑn. -4. API dรถkรผmantasyonunu รงevirerek destek olabilirsiniz [Crowdin](https://crowdin.com/project/gofiber) [![Crowdin](https://badges.crowdin.net/gofiber/localized.svg)](https://crowdin.com/project/gofiber) -5. Projeye [bir fincan kahve] ฤฑsmarlayarak projeye destek olabilirsiniz(https://buymeacoff.ee/fenny). - -## โ˜• Destekรงiler -Fiber, alan adฤฑ, gitbook, netlify, serverless yer saฤŸlayฤฑcฤฑsฤฑ giderleri ve benzeri ลŸeyleri รถdemek iรงin baฤŸฤฑลŸlarla yaลŸayan bir aรงฤฑk kaynaklฤฑ projedir. EฤŸer Fiber'e destek olmak isterseniz, โ˜• [**buradan kahve ฤฑsmarlayabilirsiniz.**](https://buymeacoff.ee/fenny) - -| | User | Donation | -| :---------------------------------------------------------- | :----------------------------------------------- | :-------- | -| ![](https://avatars.githubusercontent.com/u/204341?s=25 ) | [@destari](https://github.com/destari) | โ˜• x 10 | -| ![](https://avatars.githubusercontent.com/u/63164982?s=25 ) | [@dembygenesis](https://github.com/dembygenesis) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/56607882?s=25 ) | [@thomasvvugt](https://github.com/thomasvvugt) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/27820675?s=25 ) | [@hendratommy](https://github.com/hendratommy) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/1094221?s=25 ) | [@ekaputra07](https://github.com/ekaputra07) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/194590?s=25 ) | [@jorgefuertes](https://github.com/jorgefuertes) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/186637?s=25 ) | [@candidosales](https://github.com/candidosales) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/29659953?s=25 ) | [@l0nax](https://github.com/l0nax) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/59947262?s=25 ) | [@ankush](https://github.com/ankush) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/635852?s=25 ) | [@bihe](https://github.com/bihe) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/307334?s=25 ) | [@justdave](https://github.com/justdave) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/11155743?s=25 ) | [@koddr](https://github.com/koddr) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/29042462?s=25 ) | [@lapolinar](https://github.com/lapolinar) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/2978730?s=25 ) | [@diegowifi](https://github.com/diegowifi) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/44171355?s=25 ) | [@ssimk0](https://github.com/ssimk0) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/5638101?s=25 ) | [@raymayemir](https://github.com/raymayemir) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/619996?s=25 ) | [@melkorm](https://github.com/melkorm) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31022056?s=25 ) | [@marvinjwendt](https://github.com/thomasvvugt) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31921460?s=25 ) | [@toishy](https://github.com/toishy) | โ˜• x 1 | - -## โ€Žโ€๐Ÿ’ป Koda Katkฤฑ SaฤŸlayanlar - -Code Contributors - -## โญ๏ธ Stargazers - -Stargazers over time - -## โš ๏ธ Lisans - -Telif (c) 2019-gรผnรผmรผz [Fenny](https://github.com/fenny) ve [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber`, [MIT Lisansฤฑ](https://github.com/gofiber/fiber/blob/master/LICENSE) altฤฑnda รถzgรผr ve aรงฤฑk kaynaklฤฑ bir yazฤฑlฤฑmdฤฑr. Resmi logosu [Vic Shรณstak](https://github.com/koddr) tarafฤฑnda tasarlanmฤฑลŸtฤฑr ve [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) lisansฤฑ altฤฑnda daฤŸฤฑtฤฑmฤฑ yapฤฑlฤฑr. (CC BY-SA 4.0 International). - -**3. Parti yazฤฑlฤฑm lisanlarฤฑ** -- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) -- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) -- [fasttemplate](https://github.com/valyala/fasttemplate/blob/master/LICENSE) -- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) diff --git a/.github/README_zh-CN.md b/.github/README_zh-CN.md deleted file mode 100644 index 474a43ac..00000000 --- a/.github/README_zh-CN.md +++ /dev/null @@ -1,593 +0,0 @@ -

- - Fiber - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -

-

- Fiberๆ˜ฏไธ€ไธชๅ—ๅˆฐExpressๅฏๅ‘็š„Webๆก†ๆžถ๏ผŒๅปบ็ซ‹ๅœจGo่ฏญ่จ€ๅ†™็š„ๆœ€ๅฟซ็š„FasthttpHTTPๅผ•ๆ“Ž็š„ๅŸบ็ก€ไธŠใ€‚ๆ—จๅœจ็ฎ€ๅŒ– ้›ถๅ†…ๅญ˜ๅˆ†้…ๅ’Œๆ้ซ˜ๆ€ง่ƒฝ๏ผŒไปฅไพฟๅฟซ้€Ÿๅผ€ๅ‘ใ€‚ -

- -## โšก๏ธ ๅฟซ้€Ÿๅ…ฅ้—จ - -```go -package main - -import "github.com/gofiber/fiber" - -func main() { - app := fiber.New() - - app.Get("/", func(c *fiber.Ctx) { - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -## ๐Ÿค– ๆ€ง่ƒฝ - -่ฟ™ไบ›ๆต‹่ฏ•็”ฑ[TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext)ๅ’Œ[Go Web](https://github.com/smallnest/go-web-framework-benchmark) ๆ‰ง่กŒใ€‚ๅฆ‚ๆžœ่ฆๆŸฅ็œ‹ๆ‰€ๆœ‰็ป“ๆžœ๏ผŒ่ฏท่ฎฟ้—ฎๆˆ‘ไปฌ็š„[Wiki](https://docs.gofiber.io/benchmarks) ใ€‚ - -

- - -

- -## โš™๏ธ ๅฎ‰่ฃ… - -้ฆ–ๅ…ˆ๏ผŒ [ไธ‹่ฝฝ](https://golang.org/dl/)ๅนถๅฎ‰่ฃ…Goใ€‚ ้œ€่ฆ`1.11`ๆˆ–ๆ›ด้ซ˜็‰ˆๆœฌใ€‚ - -ไฝฟ็”จ[`go get`](https://golang.org/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them)ๅ‘ฝไปคๅฎŒๆˆๅฎ‰่ฃ…๏ผš - -```bash -export GO111MODULE=on -export GOPROXY=https://goproxy.cn - -go get -u github.com/gofiber/fiber -``` - -## ๐ŸŽฏ ็‰น็‚น - -- ๅผบๅคง็š„[่ทฏ็”ฑ](https://docs.gofiber.io/routing) -- [้™ๆ€ๆ–‡ไปถ](https://docs.gofiber.io/application#static)ๆœๅŠก -- ๆž้™[่กจ็Žฐ](https://docs.gofiber.io/benchmarks) -- [ๅ†…ๅญ˜ๅ ็”จไฝŽ](https://docs.gofiber.io/benchmarks) -- [APIๆŽฅๅฃ](https://docs.gofiber.io/context) -- [ไธญ้—ดไปถ](https://docs.gofiber.io/middleware)ๅ’Œ[Next](https://docs.gofiber.io/context#next)ๆ”ฏๆŒ -- [ๅฟซ้€Ÿ](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497)ๆœๅŠกๅ™จ็ซฏ็ผ–็จ‹ -- [ๆจก็‰ˆๅผ•ๆ“Ž](https://github.com/gofiber/template) -- [WebSocketๆ”ฏๆŒ](https://docs.gofiber.io/middleware#websocket) -- [้ข‘็Ž‡้™ๅˆถๅ™จ](https://docs.gofiber.io/middleware#limiter) -- [15็ง่ฏญ่จ€](https://docs.gofiber.io/) -- ไปฅๅŠๆ›ดๅคš่ฏท[ๆŽข็ดขๆ–‡ๆกฃ](https://docs.gofiber.io/) - -## ๐Ÿ’ก ๅ“ฒๅญฆ - -ไปŽ[Node.js](https://nodejs.org/en/about/)ๅˆ‡ๆขๅˆฐ[Go](https://golang.org/doc/)็š„ๆ–ฐ`gopher`ๅœจๅผ€ๅง‹ๆž„ๅปบ`Web`ๅบ”็”จ็จ‹ๅบๆˆ–ๅพฎๆœๅŠกไน‹ๅ‰ๆญฃๅœจๅบ”ๅฏนๅญฆไน ๆ›ฒ็บฟใ€‚ `Fiber`ไฝœไธบไธ€ไธช**Webๆก†ๆžถ** ๏ผŒๆ˜ฏๆŒ‰็…ง**ๆž็ฎ€ไธปไน‰**็š„ๆ€ๆƒณๅนถ้ตๅพช**UNIXๆ–นๅผ**ๅˆ›ๅปบ็š„๏ผŒๅ› ๆญคๆ–ฐ็š„`gopher`ๅฏไปฅๅœจ็ƒญ็ƒˆๅ’Œๅฏไฟก่ต–็š„ๆฌข่ฟŽไธญ่ฟ…้€Ÿ่ฟ›ๅ…ฅ`Go`็š„ไธ–็•Œใ€‚ - -`Fiber`ๅ—ๅˆฐไบ†ไบ’่”็ฝ‘ไธŠๆœ€ๆต่กŒ็š„`Web`ๆก†ๆžถ`Express`็š„**ๅฏๅ‘** ใ€‚ๆˆ‘ไปฌ็ป“ๅˆไบ†`Express`็š„**ๆ˜“็”จๆ€ง**ๅ’Œ`Go`็š„**ๅŽŸๅง‹ๆ€ง่ƒฝ** ใ€‚ๅฆ‚ๆžœๆ‚จๆ›พ็ปๅœจ`Node.js`ไธŠๅฎž็Žฐ่ฟ‡`Web`ๅบ”็”จ็จ‹ๅบ(*ไฝฟ็”จExpressๆˆ–็ฑปไผผๅทฅๅ…ท*)๏ผŒ้‚ฃไนˆ่ฎธๅคšๆ–นๆณ•ๅ’ŒๅŽŸ็†ๅฏนๆ‚จๆฅ่ฏดๅบ”่ฏฅ**้žๅธธๆ˜“ๆ‡‚**ใ€‚ - -ๆˆ‘ไปฌ**ๅ…ณๆณจ** _ๆ•ดไธชไบ’่”็ฝ‘_ ็”จๆˆทๅœจ[issues](https://github.com/gofiber/fiber/issues)ๅ’ŒDiscord [channel](https://gofiber.io/discord)็š„ๆถˆๆฏ๏ผŒไธบไบ†ๅˆ›ๅปบไธ€ไธช**่ฟ…้€Ÿ**๏ผŒ**็ตๆดป**ไปฅๅŠ**ๅ‹ๅฅฝ**็š„`Go web`ๆก†ๆžถ๏ผŒๆปก่ถณ**ไปปไฝ•**ไปปๅŠก๏ผŒ**ๆœ€ๅŽๆœŸ้™**ๅ’Œๅผ€ๅ‘่€…**ๆŠ€่ƒฝ**ใ€‚ๅฐฑๅƒ`Express`ๅœจ`JavaScript`ไธ–็•Œไธญไธ€ๆ ทใ€‚ - -## ๐Ÿ‘€ ็คบไพ‹ - -ไธ‹้ขๅˆ—ๅ‡บไบ†ไธ€ไบ›ๅธธ่ง็คบไพ‹ใ€‚ๅฆ‚ๆžœๆ‚จๆƒณๆŸฅ็œ‹ๆ›ดๅคšไปฃ็ ็คบไพ‹๏ผŒ่ฏท่ฎฟ้—ฎๆˆ‘ไปฌ็š„[Recipes](https://github.com/gofiber/recipes)ไปฃ็ ๅบ“ๆˆ–[APIๆ–‡ๆกฃ](https://docs.gofiber.io) ใ€‚ - -#### ๐Ÿ“– [**ๅŸบ็ก€่ทฏ็”ฑ**](https://docs.gofiber.io/#basic-routing) - -```go -func main() { - app := fiber.New() - - // GET /john - app.Get("/:name", func(c *fiber.Ctx) { - msg := fmt.Sprintf("Hello, %s ๐Ÿ‘‹!", c.Params("name")) - c.Send(msg) // => Hello john ๐Ÿ‘‹! - }) - - // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ‘ด %s is %s years old", c.Params("name"), c.Params("age")) - c.Send(msg) // => ๐Ÿ‘ด john is 75 years old - }) - - // GET /dictionary.txt - app.Get("/:file.:ext", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ“ƒ %s.%s", c.Params("file"), c.Params("ext")) - c.Send(msg) // => ๐Ÿ“ƒ dictionary.txt - }) - - // GET /flights/LAX-SFO - app.Get("/flights/:from-:to", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ’ธ From: %s, To: %s", c.Params("from"), c.Params("to")) - c.Send(msg) // => ๐Ÿ’ธ From: LAX, To: SFO - }) - - // GET /api/register - app.Get("/api/*", func(c *fiber.Ctx) { - msg := fmt.Sprintf("โœ‹ %s", c.Params("*")) - c.Send(msg) // => โœ‹ /api/register - }) - - app.Listen(3000) -} -``` - -#### ๐Ÿ“– [**้™ๆ€ๆ–‡ไปถ**](https://docs.gofiber.io/application#static)ๆœๅŠก - -```go -func main() { - app := fiber.New() - - app.Static("/", "./public") - // => http://localhost:3000/js/script.js - // => http://localhost:3000/css/style.css - - app.Static("/prefix", "./public") - // => http://localhost:3000/prefix/js/script.js - // => http://localhost:3000/prefix/css/style.css - - app.Static("*", "./public/index.html") - // => http://localhost:3000/any/path/shows/index/html - - app.Listen(3000) -} -``` - -#### ๐Ÿ“– [**ไธญ้—ดไปถ**](https://docs.gofiber.io/middleware)ๅ’Œ[**Next**](https://docs.gofiber.io/context#next) - -```go -func main() { - app := fiber.New() - - // Match any route - app.Use(func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‡ First handler") - c.Next() - }) - - // Match all routes starting with /api - app.Use("/api", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅˆ Second handler") - c.Next() - }) - - // GET /api/register - app.Get("/api/list", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‰ Last handler") - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -
- ๐Ÿ“š ๅฑ•็คบๆ›ดๅคšไปฃ็ ็คบไพ‹ - -### ๆจก็‰ˆๅผ•ๆ“Ž - -๐Ÿ“– [้…็ฝฎ](https://docs.gofiber.io/application#settings) -๐Ÿ“– [ๆจก็‰ˆๅผ•ๆ“Ž](https://github.com/gofiber/template) -๐Ÿ“– [ๆธฒๆŸ“](https://docs.gofiber.io/context#render) - -ๅฆ‚ๆžœๆœช่ฎพ็ฝฎๆจก็‰ˆๅผ•ๆ“Ž๏ผŒๅˆ™`Fiber`้ป˜่ฎคไฝฟ็”จ[html/template](https://golang.org/pkg/html/template/)ใ€‚ - -ๅฆ‚ๆžœๆ‚จ่ฆๆ‰ง่กŒ้ƒจๅˆ†ๆจก็‰ˆๆˆ–ไฝฟ็”จๅ…ถไป–ๅผ•ๆ“Ž๏ผŒไพ‹ๅฆ‚[amber](https://github.com/eknkc/amber)๏ผŒ[handlebars](https://github.com/aymerick/raymond)๏ผŒ[mustache](https://github.com/cbroglie/mustache)ๆˆ–่€…[pug](https://github.com/Joker/jade)็ญ‰็ญ‰... - -่ฏทๆŸฅ็œ‹ๆˆ‘ไปฌ็š„[Template](https://github.com/gofiber/template)ๅŒ…๏ผŒ่ฏฅๅŒ…ๆ”ฏๆŒๅคšไธชๆจก็‰ˆๅผ•ๆ“Žใ€‚ - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/template/pug" -) - -func main() { - // You can setup Views engine before initiation app: - app := fiber.New(&fiber.Settings{ - Views: pug.New("./views", ".pug"), - }) - - // OR after initiation app at any convenient location: - app.Settings.Views = pug.New("./views", ".pug"), - - // And now, you can call template `./views/home.pug` like this: - app.Get("/", func(c *fiber.Ctx) { - c.Render("home", fiber.Map{ - "title": "Homepage", - "year": 1999, - }) - }) - - // ... -} -``` - -### ็ป„ๅˆ่ทฏ็”ฑ้“พ - -๐Ÿ“– [่ทฏ็”ฑๅˆ†็ป„](https://docs.gofiber.io/application#group) - -```go -func main() { - app := fiber.New() - - // Root API route - api := app.Group("/api", cors()) // /api - - // API v1 routes - v1 := api.Group("/v1", mysql()) // /api/v1 - v1.Get("/list", handler) // /api/v1/list - v1.Get("/user", handler) // /api/v1/user - - // API v2 routes - v2 := api.Group("/v2", mongodb()) // /api/v2 - v2.Get("/list", handler) // /api/v2/list - v2.Get("/user", handler) // /api/v2/user - - // ... -} -``` - -### ๆ—ฅๅฟ—ไธญ้—ดไปถ - -๐Ÿ“– [Logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - // Default - app.Use(middleware.Logger()) - - // Custom logging format - app.Use(middleware.Logger("${method} - ${path}")) - - // Custom Config - app.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ - Next: func(ctx *fiber.Ctx) bool { - return ctx.Path() != "/private" - }, - Format: "${method} - ${path}", - Output: io.Writer, - })) - - app.Listen(3000) -} -``` -### ่ทจๅŸŸ่ต„ๆบๅ…ฑไบซ(CORS)ไธญ้—ดไปถ - -๐Ÿ“– [CORS](https://docs.gofiber.io/middleware#cors) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/cors" -) - -func main() { - app := fiber.New() - - // CORS with default config - app.Use(cors.New()) - - app.Listen(3000) -} -``` - -้€š่ฟ‡ๅœจ่ฏทๆฑ‚ๅคดไธญ่ฎพ็ฝฎ`Origin`ไผ ้€’ไปปไฝ•ๅŸŸๆฅๆฃ€ๆŸฅCORS๏ผš - -```bash -curl -H "Origin: http://example.com" --verbose http://localhost:3000 -``` - -### ่‡ชๅฎšไน‰404ๅ“ๅบ” - -๐Ÿ“– [HTTP Methods](https://docs.gofiber.io/application#http-methods) - -```go -func main() { - app := fiber.New() - - app.Static("./public") - - app.Get("/demo", func(c *fiber.Ctx) { - c.Send("This is a demo!") - }) - - app.Post("/register", func(c *fiber.Ctx) { - c.Send("Welcome!") - }) - - // Last middleware to match anything - app.Use(func(c *fiber.Ctx) { - c.SendStatus(404) - // => 404 "Not Found" - }) - - app.Listen(3000) -} -``` - -### JSONๅ“ๅบ” - -๐Ÿ“– [JSON](https://docs.gofiber.io/context#json) - -```go -type User struct { - Name string `json:"name"` - Age int `json:"age"` -} - -func main() { - app := fiber.New() - - app.Get("/user", func(c *fiber.Ctx) { - c.JSON(&User{"John", 20}) - // => {"name":"John", "age":20} - }) - - app.Get("/json", func(c *fiber.Ctx) { - c.JSON(fiber.Map{ - "success": true, - "message": "Hi John!", - }) - // => {"success":true, "message":"Hi John!"} - }) - - app.Listen(3000) -} -``` - -### ๅ‡็บงๅˆฐWebSocket - -๐Ÿ“– [Websocket](https://docs.gofiber.io/middleware#websocket) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/websocket" -) - -func main() { - app := fiber.New() - - app.Get("/ws", websocket.New(func(c *websocket.Conn) { - for { - mt, msg, err := c.ReadMessage() - if err != nil { - log.Println("read:", err) - break - } - log.Printf("recv: %s", msg) - err = c.WriteMessage(mt, msg) - if err != nil { - log.Println("write:", err) - break - } - } - })) - - app.Listen(3000) - // ws://localhost:3000/ws -} -``` - -### ๆขๅค(panic)ไธญ้—ดไปถ - -๐Ÿ“– [Recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - app.Use(middleware.Recover()) - - app.Get("/", func(c *fiber.Ctx) { - panic("normally this would crash your app") - }) - - app.Listen(3000) -} -``` -
- -## ๐Ÿงฌ Fiberไธญ้—ดไปถ - -ๆญคๅค„ๅˆ—ๅ‡บ็š„`Fiber`ไธญ้—ดไปถๆจกๅ—็”ฑ[Fiberๅ›ข้˜Ÿ](https://github.com/orgs/gofiber/people)็ปดๆŠคใ€‚ - -| ไธญ้—ดไปถ | ๆ่ฟฐ | ๅ†…็ฝฎไธญ้—ดไปถ | -| :--- | :--- | :--- | -| [adaptor](https://github.com/gofiber/adaptor) | `net/http`ๅค„็†็จ‹ๅบไธŽ`Fiber`ๅค„็†็จ‹ๅบไน‹้—ด็š„้€‚้…ๅ™จ๏ผŒ็‰นๅˆซๆ„Ÿ่ฐข @arsmn! | - | -| [basicauth](https://github.com/gofiber/basicauth) | ๅŸบๆœฌ่บซไปฝ้ชŒ่ฏไธญ้—ดไปถๆไพ›HTTPๅŸบๆœฌ่บซไปฝ้ชŒ่ฏใ€‚้ชŒ่ฏๆœ‰ๆ•ˆๆ—ถ๏ผŒๅฎƒ่ฐƒ็”จไธ‹ไธ€ไธชๅค„็†็จ‹ๅบ๏ผŒๅฆๅˆ™่ฟ”ๅ›ž`401 Unauthorized`ๅ“ๅบ”ใ€‚ | - | -| [compress](https://github.com/gofiber/fiber/blob/master/middleware/compress.md) | `Fiber`็š„ๅŽ‹็ผฉไธญ้—ดไปถ๏ผŒ้ป˜่ฎคๆƒ…ๅ†ตไธ‹ๆ”ฏๆŒ`deflate`๏ผŒ`gzip`ๅ’Œ`brotli`ใ€‚ | `middleware.Compress()` | -| [cors](https://github.com/gofiber/cors) | ไฝฟ็”จๅ„็ง้€‰้กนๅฏ็”จ่ทจๅŸŸ่ต„ๆบๅ…ฑไบซ๏ผˆCORS๏ผ‰ใ€‚ | - | -| [csrf](https://github.com/gofiber/csrf) | ไฟๆŠคๅ…ๅ—CSRFๆ”ปๅ‡ปใ€‚ | - | -| [filesystem](https://github.com/gofiber/fiber/blob/master/middleware/filesystem.md) | `Fiber`็š„FileSystemไธญ้—ดไปถ๏ผŒ็‰นๅˆซๆ„Ÿ่ฐข Alireza Salary | - | -| [favicon](https://github.com/gofiber/fiber/blob/master/middleware/favicon.md) | ๅฆ‚ๆžœๆไพ›ไบ†`favicon`ๆ–‡ไปถ่ทฏๅพ„๏ผŒๅˆ™ๅฟฝ็•ฅๆ—ฅๅฟ—ไธญ็š„ๅ›พๆ ‡ๆˆ–ไปŽๅ†…ๅญ˜ไธญๆไพ›ๅ›พๆ ‡ใ€‚ | `middleware.Favicon()` | -| [helmet](https://github.com/gofiber/helmet) | ้€š่ฟ‡่ฎพ็ฝฎๅ„็งHTTPๆ ‡ๅคดๆฅไฟๆŠคๆ‚จ็š„ๅบ”็”จ็จ‹ๅบใ€‚ | - | -| [jwt](https://github.com/gofiber/jwt) | JWTๆ˜ฏ่ฟ”ๅ›žJSON Webไปค็‰Œ๏ผˆJWT๏ผ‰็š„่บซไปฝ้ชŒ่ฏไธญ้—ดไปถใ€‚ | - | -| [keyauth](https://github.com/gofiber/keyauth) | ๅฏ†้’ฅ่บซไปฝ้ชŒ่ฏไธญ้—ดไปถๆไพ›ๅŸบไบŽๅฏ†้’ฅ็š„่บซไปฝ้ชŒ่ฏใ€‚ | - | -| [limiter](https://github.com/gofiber/limiter) | `Fiber`็š„้ข‘็Ž‡้™ๅˆถไธญ้—ดไปถใ€‚็”จไบŽ้™ๅˆถๅฏนๅ…ฌๅ…ฑAPI็š„้‡ๅค่ฏทๆฑ‚๏ผŒไพ‹ๅฆ‚ๅฏ†็ ้‡็ฝฎใ€‚ | - | -| [logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) | HTTP ่ฎฟ้—ฎๆ—ฅๅฟ—ไธญ้—ดไปถใ€‚ | `middleware.Logger()` | -| [pprof](https://github.com/gofiber/pprof) | ็‰นๅˆซๆ„Ÿ่ฐข Matthew Lee ๏ผˆ@mthli๏ผ‰ | - | -| [recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) | ๆขๅคไธญ้—ดไปถๅฏไปŽๅ †ๆ ˆ้“พไธญไปปไฝ•ๅœฐๆ–น็š„`panic`ไธญๆขๅค๏ผŒๅนถๅฐ†ๆŽงๅˆถๆƒไบค็ป™้›†ไธญๅผ[้”™่ฏฏๅค„็†ๅ™จ](error-handling.md). | `middleware.Recover()` | -| [rewrite](https://github.com/gofiber/rewrite) | `Rewrite`ไธญ้—ดไปถๆ นๆฎๆไพ›็š„่ง„ๅˆ™้‡ๅ†™`URL`่ทฏๅพ„ใ€‚ๅฎƒๅฏนไบŽๅ‘ๅŽๅ…ผๅฎนๆˆ–ๅˆ›ๅปบๆ›ด็ฎ€ๆด๏ผŒๆ›ดๅ…ทๆ่ฟฐๆ€ง็š„้“พๆŽฅๅพˆๆœ‰ๅธฎๅŠฉใ€‚ | - | -| [requestid](https://github.com/gofiber/fiber/blob/master/middleware/request_id.md) | Request IDไธญ้—ดไปถไธบ่ฏทๆฑ‚็”Ÿๆˆๅ”ฏไธ€็š„IDใ€‚ | `middleware.RequestID()` | -| [session](https://github.com/gofiber/session) | `session`ไธญ้—ดไปถ้€š่ฟ‡ไบ† @savsgio MIT ่ฎธๅฏ๏ผŒๅปบ็ซ‹ๅœจ`fasthttp/session`ไน‹ไธŠใ€‚็‰นๅˆซๆ„Ÿ่ฐข @thomasvvugt ๅธฎๅŠฉๅฎŒๆˆๆญคไธญ้—ดไปถใ€‚ | - | -| [template](https://github.com/gofiber/template) | ่ฏฅ่ฝฏไปถๅŒ…ๅŒ…ๅซ8ไธชๆจกๆฟๅผ•ๆ“Ž๏ผŒ้œ€่ฆ้…ๅˆFiber`v1.10.x`ไปฅๅŠGo`1.13`ๆˆ–ๆ›ด้ซ˜็‰ˆๆœฌไฝฟ็”จใ€‚ | - | -| [websocket](https://github.com/gofiber/websocket) | `Fiber`ๅŸบไบŽ`Fasthttp WebSocket`็š„ไธญ้—ดไปถ๏ผŒๆ”ฏๆŒ`Locals`ๅŠŸ่ƒฝ๏ผ | - | - -## ๐ŸŒฑ ็ฌฌไธ‰ๆ–นไธญ้—ดไปถ - -่ฟ™ๆ˜ฏ็”ฑ`Fiber`็คพๅŒบๅˆ›ๅปบ็š„ไธญ้—ดไปถๅˆ—่กจ๏ผŒๅฆ‚ๆžœๆ‚จๆƒณ็œ‹ๅˆฐ่‡ชๅทฑ็š„ไธญ้—ดไปถ๏ผŒ่ฏทๅˆ›ๅปบ`PR`ใ€‚ -- [arsmn/fiber-casbin](https://github.com/arsmn/fiber-casbin) -- [arsmn/fiber-introspect](https://github.com/arsmn/fiber-introspect) -- [arsmn/fiber-swagger](https://github.com/arsmn/fiber-swagger) -- [arsmn/gqlgen](https://github.com/arsmn/gqlgen) -- [codemicro/fiber-cache](https://github.com/codemicro/fiber-cache) -- [itsursujit/fiber-boilerplate](https://github.com/itsursujit/fiber-boilerplate) -- [juandiii/go-jwk-security](https://github.com/juandiii/go-jwk-security) -- [kiyonlin/fiber_limiter](https://github.com/kiyonlin/fiber_limiter) -- [shareed2k/fiber_limiter](https://github.com/shareed2k/fiber_limiter) -- [shareed2k/fiber_tracing](https://github.com/shareed2k/fiber_tracing) -- [thomasvvugt/fiber-boilerplate](https://github.com/thomasvvugt/fiber-boilerplate) - -## ๐Ÿ’ฌ ๅช’ไฝ“ - -

- - -

- -- [Welcome to Fiber โ€” an Express.js styled web framework written in Go with โค๏ธ](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) โ€” _03 Feb 2020_ -- [Fiber released v1.7! ๐ŸŽ‰ What's new and is it still fast, flexible and friendly?](https://dev.to/koddr/fiber-v2-is-out-now-what-s-new-and-is-he-still-fast-flexible-and-friendly-3ipf) โ€” _21 Feb 2020_ -- [๐Ÿš€ Fiber v1.8. What's new, updated and re-thinked?](https://dev.to/koddr/fiber-v1-8-what-s-new-updated-and-re-thinked-339h) โ€” _03 Mar 2020_ -- [Is switching from Express to Fiber worth it? ๐Ÿค”](https://dev.to/koddr/are-sure-what-your-lovely-web-framework-running-so-fast-2jl1) โ€” _01 Apr 2020_ -- [Creating Fast APIs In Go Using Fiber](https://dev.to/jozsefsallai/creating-fast-apis-in-go-using-fiber-59m9) โ€” _07 Apr 2020_ -- [Building a Basic REST API in Go using Fiber](https://tutorialedge.net/golang/basic-rest-api-go-fiber/) - _23 Apr 2020_ -- [๐Ÿ“บ Building a REST API using GORM and Fiber](https://youtu.be/Iq2qT0fRhAA) - _25 Apr 2020_ -- [๐ŸŒŽ Create a travel list app with Go, Fiber, Angular, MongoDB and Google Cloud Secret Manager](https://blog.yongweilun.me/create-a-travel-list-app-with-go-fiber-angular-mongodb-and-google-cloud-secret-manager-ck9fgxy0p061pcss1xt1ubu8t) - _25 Apr 2020_ -- [Fiber v1.9.6 ๐Ÿ”ฅ How to improve performance by 817% and stay fast, flexible and friendly?](https://dev.to/koddr/fiber-v1-9-5-how-to-improve-performance-by-817-and-stay-fast-flexible-and-friendly-2dp6) - _12 May 2020_ -- [The road to web-based authentication with Fiber โšก](https://vugt.me/the-road-to-web-based-authentication-with-fiber/) - _20 May 2020_ -- [Building an Express-style API in Go with Fiber](https://blog.logrocket.com/express-style-api-go-fiber/) - _10 June 2020_ -- [ๅŸบไบŽgolang fiberๅ’Œangularๅผ€ๅ‘web](https://zhuanlan.zhihu.com/p/148925642) - _19 June 2020_ -- [ๅŸบไบŽๅปถ่ฟŸ่ฎก็ฎ—ไปค็‰Œๆกถ็š„gofiber้ข‘็Ž‡้™ๅˆถไธญ้—ดไปถๅฎž็Žฐ](https://zhuanlan.zhihu.com/p/149308936) - _20 June 2020_ -- [Construir una API en Golang con Fiber ๐Ÿ‡ช๐Ÿ‡ธ](https://enbonnet.me/article/53/construir-api-golang-con-fiber) - _28 June 2020_ -- [๐Ÿ“บWhy Go Fiber Is THE New Framework To Learn](https://www.youtube.com/watch?v=kvwsPeWDLM8) - _29 June 2020_ -- [่งฃๆžGofiber่ทฏ็”ฑ็ฎก็†](https://zhuanlan.zhihu.com/p/152494502) - _08 July 2020_ -- [๐Ÿ“บ Introduction to Fiber - An Express-inspired web framework](https://youtu.be/MfFi4Gt-tos) - _25 July 2020_ - -## ๐Ÿ‘ ่ดก็Œฎ - -ๅฆ‚ๆžœๆ‚จ่ฆ่ฏดๅฃฐ**่ฐข่ฐข**ๆˆ–ๆ”ฏๆŒ`Fiber`็š„็งฏๆžๅ‘ๅฑ•๏ผš - -1. ไธบ`Fiber`[GitHub Star](https://github.com/gofiber/fiber/stargazers)็‚นไธชโญๆ˜Ÿๆ˜Ÿใ€‚ -2. ๅœจ[Twitter](https://twitter.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber)ไธŠๅ‘ๅธƒๆœ‰ๅ…ณ้กน็›ฎ็š„[ๆŽจๆ–‡](https://twitter.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber)ใ€‚ -3. ๅœจ[Medium](https://medium.com/)๏ผŒ[Dev.to](https://dev.to/)ๆˆ–ไธชไบบๅšๅฎขไธŠๅ†™่ฏ„่ฎบๆˆ–ๆ•™็จ‹ใ€‚ -4. ๅธฎๅŠฉๆˆ‘ไปฌ้€š่ฟ‡[Crowdin](https://crowdin.com/project/gofiber) [![Crowdin](https://badges.crowdin.net/gofiber/localized.svg)](https://crowdin.com/project/gofiber)็ฟป่ฏ‘ๆˆ‘ไปฌ็š„APIๆ–‡ๆกฃ -5. ้€š่ฟ‡ๆ่ต [ไธ€ๆฏๅ’–ๅ•ก](https://buymeacoff.ee/fenny)ๆฅๆ”ฏๆŒๆœฌ้กน็›ฎใ€‚ - -## โ˜• ๆ”ฏๆŒ่€… - -`Fibre`ๆ˜ฏไธ€ไธชๅผ€ๆบ้กน็›ฎ๏ผŒไพ้ ๆ่ต ๆฅๆ”ฏไป˜่ดฆๅ•๏ผŒไพ‹ๅฆ‚ๆˆ‘ไปฌ็š„ๅŸŸๅ๏ผŒ`gitbook`๏ผŒ`netlify`ๅ’Œๆ— ๆœๅŠกๅ™จๆ‰˜็ฎกใ€‚ๅฆ‚ๆžœ่ฆๆ”ฏๆŒ`Fiber`๏ผŒๅฏไปฅ โ˜• [**ๅœจ่ฟ™้‡Œไนฐไธ€ๆฏๅ’–ๅ•ก**](https://buymeacoff.ee/fenny) - -| | User | Donation | -| :---------------------------------------------------------- | :----------------------------------------------- | :-------- | -| ![](https://avatars.githubusercontent.com/u/204341?s=25 ) | [@destari](https://github.com/destari) | โ˜• x 10 | -| ![](https://avatars.githubusercontent.com/u/63164982?s=25 ) | [@dembygenesis](https://github.com/dembygenesis) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/56607882?s=25 ) | [@thomasvvugt](https://github.com/thomasvvugt) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/27820675?s=25 ) | [@hendratommy](https://github.com/hendratommy) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/1094221?s=25 ) | [@ekaputra07](https://github.com/ekaputra07) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/194590?s=25 ) | [@jorgefuertes](https://github.com/jorgefuertes) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/186637?s=25 ) | [@candidosales](https://github.com/candidosales) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/29659953?s=25 ) | [@l0nax](https://github.com/l0nax) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/59947262?s=25 ) | [@ankush](https://github.com/ankush) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/635852?s=25 ) | [@bihe](https://github.com/bihe) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/307334?s=25 ) | [@justdave](https://github.com/justdave) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/11155743?s=25 ) | [@koddr](https://github.com/koddr) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/29042462?s=25 ) | [@lapolinar](https://github.com/lapolinar) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/2978730?s=25 ) | [@diegowifi](https://github.com/diegowifi) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/44171355?s=25 ) | [@ssimk0](https://github.com/ssimk0) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/5638101?s=25 ) | [@raymayemir](https://github.com/raymayemir) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/619996?s=25 ) | [@melkorm](https://github.com/melkorm) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31022056?s=25 ) | [@marvinjwendt](https://github.com/thomasvvugt) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31921460?s=25 ) | [@toishy](https://github.com/toishy) | โ˜• x 1 | - -## โ€Žโ€๐Ÿ’ป Code Contributors - -Code Contributors - -## โญ๏ธ Stargazers - -Stargazers over time - -## โš ๏ธ License - -Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` is free and open-source software licensed under the [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Official logo was created by [Vic Shรณstak](https://github.com/koddr) and distributed under [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) license (CC BY-SA 4.0 International). - -**Third-party library licenses** -- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) -- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) -- [fasttemplate](https://github.com/valyala/fasttemplate/blob/master/LICENSE) -- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) diff --git a/.github/README_zh-TW.md b/.github/README_zh-TW.md deleted file mode 100644 index c27c74c7..00000000 --- a/.github/README_zh-TW.md +++ /dev/null @@ -1,591 +0,0 @@ -

- - Fiber - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -

-

- Fiberๆ˜ฏ็งปๆคNodeJS็š„Expressๆก†ๆžถๆ”นไปฅGo่ชž่จ€็ทจๅฏซใ€‚ๆœฌๅฅ—ไปถๅŸบๆ–ผFasthttp๏ผŒFasthttpๆœ‰ไธๅˆ†้…่จ˜ๆ†ถ้ซ”็ฉบ้–“ๅ’ŒRequest Pool็š„็‰นๆ€ง๏ผŒๅœจ็ถฒ่ทฏๆ•ˆ่ƒฝๆ–น้ขๆœ‰่‘—้กฏ่‘—็š„ๆ•ˆ่ƒฝใ€‚ -

- -## โšก๏ธ ๅฟซ้€Ÿๅ…ฅ้–€ - -```go -package main - -import "github.com/gofiber/fiber" - -func main() { - app := fiber.New() - - app.Get("/", func(c *fiber.Ctx) { - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -## ๐Ÿค– ๆ•ˆ่ƒฝ - -ๆœฌๆธฌ่ฉฆไฝฟ็”จ[TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext)ๅ’Œ[Go Webๆก†ๆžถๆ•ˆ่ƒฝๆธฌ่ฉฆ](https://github.com/smallnest/go-web-framework-benchmark)ใ€‚ๅฆ‚ๆžœ่ฆ็œ‹ๅ…จ้ƒจ็š„ๅŸท่กŒ็ตๆžœ๏ผŒ่ซ‹ๅˆฐ[Wiki](https://docs.gofiber.io/benchmarks) ใ€‚ - -

- - -

- -## โš™๏ธ ๅฎ‰่ฃ - -็ฌฌไธ€ๆญฅ๏ผŒ [ไธ‹่ผ‰](https://golang.org/dl/)ไธฆๅฎ‰่ฃGoใ€‚ ่ซ‹ไฝฟ็”จ`1.11`ไปฅไธŠ็‰ˆๆœฌใ€‚ - -ไธ‹ๆŒ‡ไปค[`go get`](https://golang.org/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them)ไธ‹่ผ‰ๅฅ—ไปถ๏ผš - -```bash -go get -u github.com/gofiber/fiber -``` - -## ๐ŸŽฏ ๅŠŸ่ƒฝ - -- ๅผทๅคง็š„[่ทฏ็”ฑ](https://docs.gofiber.io/routing) -- [้œๆ…‹ๆช”ๆกˆ](https://docs.gofiber.io/application#static)ๆœๅ‹™ -- [่ถ…ๅฟซ้€Ÿ](https://docs.gofiber.io/benchmarks) -- [ไฝ”็”จๅพˆๅฐ‘่จ˜ๆ†ถ้ซ”](https://docs.gofiber.io/benchmarks) -- ๆ”ฏๆดExpress็š„[API](https://docs.gofiber.io/context) -- ๆ”ฏๆดไธญไป‹ๅ™จๅ’Œ[ไธ‹ไธ€ๆญฅ](https://docs.gofiber.io/context#next) -- [็ซ‹ๅณไธŠๆ‰‹](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) -- [ๆจฃๆฟๅผ•ๆ“Ž](https://github.com/gofiber/template) -- ๆ”ฏๆด[WebSocket](https://docs.gofiber.io/middleware#websocket) -- ๆ”ฏๆด[้™้€Ÿ](https://docs.gofiber.io/middleware#limiter) -- ่ขซ็ฟป่ญฏๆˆ[15]ๅœ‹่ชž่จ€(https://docs.gofiber.io/) -- ่ฑๅฏŒ็š„[ๆ–‡ไปถ](https://docs.gofiber.io/) - -## ๐Ÿ’ก ็†ๅฟต - -ไธๅฐ‘[Node.js](https://nodejs.org/en/about/)็š„ๅทฅ็จ‹ๅธซ่ทณๅˆฐ[Go](https://golang.org/doc/)ๅฟ…้ ˆๅญธ็ฟ’ไธ€ไบ›็Ÿฅ่ญ˜๏ผŒๅ› ๆญคๅšไบ†ไธ€ๅ€‹่ทŸExpressไธ€ๆจฃ็š„Fiber็œ้€™ไบ›้บป็…ฉใ€‚่จญ่จˆ้‚„ๆ˜ฏ็…งๅŽŸๆœฌ็š„**ๆฅต็ฐกไธป็พฉ**้‚„ๆœ‰้ตๅพช**UNIXๆ…ฃไพ‹**๏ผŒๅ› ๆญคๆ–ฐๆ‰‹ๅ€‘ๅฏไปฅ**็„ก็—›**่ฟ…้€Ÿ้€ฒๅ…ฅGo็š„ไธ–็•Œใ€‚ - -Fiber **ๅ—ๅˆฐ** ็ถฒ่ทฏไธŠๆœ€ๆต่กŒ็š„Webๆก†ๆžถExpressJS**ๅ•Ÿ็™ผ**๏ผŒ็ตๅˆExpress็š„**ๆ˜“็”จๆ€ง**ๅ’ŒGo็š„**้ซ˜ๆ•ˆ่ƒฝ**ใ€‚่‹ฅไฝ ไน‹ๅ‰็”จ้ŽNode.jsๅฏซWebๆ‡‰็”จ(*ไฝฟ็”จExpressJS/Koaๆˆ–้กžไผผๅทฅๅ…ท*)๏ผŒ้‚ฃไฝ ๅทฒ็ถ“**ไธŠๆ‰‹**ไบ†ใ€‚ - -ๆœ‰ไป€้บผๅ•้กŒ่ซ‹็™ผ[issues](https://github.com/gofiber/fiber/issues)ๆˆ–ๅŠ ๅ…ฅDiscord [channel](https://gofiber.io/discord)่จŽ่ซ–๏ผŒๆˆ‘ๅ€‘ๆƒณ่ฆๅ‰ต้€ **ๅฟซ้€Ÿ**ใ€**ๅฝˆๆ€ง**ใ€**ๅ‹ๅ–„**็š„็คพ็พค็ตฆ**ไปปไฝ•ไบบ**ไฝฟ็”จ๏ผๅฐฑๅƒExpress้‚ฃๆจฃใ€‚ - -## ๐Ÿ‘€ ็ฏ„ไพ‹ - -ไปฅไธ‹ๆ˜ฏไธ€ไบ›ๅธธ่ฆ‹็ฏ„ไพ‹ใ€‚ - -> ๆ›ดๅคš็จ‹ๅผ็ขผๅœจ[็ฏ„ไพ‹ๅฐˆๆกˆ](https://github.com/gofiber/recipes)ไธญๆˆ–็›ดๆŽฅ็œ‹[APIๆ–‡ไปถ](https://docs.gofiber.io)ใ€‚ - -#### ๐Ÿ“– [**Basic Routing**](https://docs.gofiber.io/#basic-routing) - -```go -func main() { - app := fiber.New() - - // GET /john - app.Get("/:name", func(c *fiber.Ctx) { - msg := fmt.Sprintf("Hello, %s ๐Ÿ‘‹!", c.Params("name")) - c.Send(msg) // => Hello john ๐Ÿ‘‹! - }) - - // GET /john/75 - app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ‘ด %s is %s years old", c.Params("name"), c.Params("age")) - c.Send(msg) // => ๐Ÿ‘ด john is 75 years old - }) - - // GET /dictionary.txt - app.Get("/:file.:ext", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ“ƒ %s.%s", c.Params("file"), c.Params("ext")) - c.Send(msg) // => ๐Ÿ“ƒ dictionary.txt - }) - - // GET /flights/LAX-SFO - app.Get("/flights/:from-:to", func(c *fiber.Ctx) { - msg := fmt.Sprintf("๐Ÿ’ธ From: %s, To: %s", c.Params("from"), c.Params("to")) - c.Send(msg) // => ๐Ÿ’ธ From: LAX, To: SFO - }) - - // GET /api/register - app.Get("/api/*", func(c *fiber.Ctx) { - msg := fmt.Sprintf("โœ‹ %s", c.Params("*")) - c.Send(msg) // => โœ‹ /api/register - }) - - app.Listen(3000) -} -``` - -#### ๐Ÿ“– [**Serving Static Files**](https://docs.gofiber.io/application#static) - -```go -func main() { - app := fiber.New() - - app.Static("/", "./public") - // => http://localhost:3000/js/script.js - // => http://localhost:3000/css/style.css - - app.Static("/prefix", "./public") - // => http://localhost:3000/prefix/js/script.js - // => http://localhost:3000/prefix/css/style.css - - app.Static("*", "./public/index.html") - // => http://localhost:3000/any/path/shows/index/html - - app.Listen(3000) -} -``` - -#### ๐Ÿ“– [**Middleware & Next**](https://docs.gofiber.io/context#next) - -```go -func main() { - app := fiber.New() - - // Match any route - app.Use(func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‡ First handler") - c.Next() - }) - - // Match all routes starting with /api - app.Use("/api", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅˆ Second handler") - c.Next() - }) - - // GET /api/register - app.Get("/api/list", func(c *fiber.Ctx) { - fmt.Println("๐Ÿฅ‰ Last handler") - c.Send("Hello, World ๐Ÿ‘‹!") - }) - - app.Listen(3000) -} -``` - -
- ๐Ÿ“š ้กฏ็คบๆ›ดๅคš็ฏ„ไพ‹ - -### ็•Œ้ขๅผ•ๆ“Ž - -๐Ÿ“– [่จญๅฎš](https://docs.gofiber.io/application#settings) -๐Ÿ“– [ๅผ•ๆ“Ž](https://github.com/gofiber/template) -๐Ÿ“– [ๆธฒๆŸ“](https://docs.gofiber.io/context#render) - -็•ถไธๆŒ‡ๅฎšๆจฃๆฟๅผ•ๆ“Žๆ™‚Fiber้ ่จญ็”จ[html/template](https://golang.org/pkg/html/template/)ใ€‚ - -ๅฆ‚ๆžœไฝ ๆƒณ่ฆๅŸท่กŒ้ƒจไปฝๆˆ–็”จๅˆฅ็š„ๆจฃๆฟๅผ•ๆ“Ž[amber](https://github.com/eknkc/amber)ใ€[handlebars](https://github.com/aymerick/raymond)ใ€[mustache](https://github.com/cbroglie/mustache)ใ€[pug](https://github.com/Joker/jade)ไน‹้กžโ€ฆ่ซ‹ๅƒ่€ƒ็ฌฆๅˆๅคšๆจฃๆฟๅผ•ๆ“Ž็š„[ๆจฃๆฟ](https://github.com/gofiber/template)ๅฅ—ไปถใ€‚ - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/template/pug" -) - -func main() { - // ไฝ ๅฏไปฅๅœจๅˆๅง‹ๅŒ–ๅ‰ๅฅ—็”จๆจฃๆฟๅผ•ๆ“Ž - app := fiber.New(&fiber.Settings{ - Views: pug.New("./views", ".pug"), - }) - - // ๆˆ–ๅœจๅˆๅง‹ๅŒ–ๅพŒไปปๆ„ๆ™‚้–“ๅฅ—็”จ: - app.Settings.Views = pug.New("./views", ".pug"), - - // ็พๅœจ๏ผŒไฝ ๅฏไปฅๅƒ้€™ๆจฃๅ‘ผๅซๆจฃๆฟ `./views/home.pug`: - app.Get("/", func(c *fiber.Ctx) { - c.Render("home", fiber.Map{ - "title": "้ฆ–้ ", - "year": 1999, - }) - }) - - // ... -} -``` - -### Grouping routes into chains - -๐Ÿ“– [Group](https://docs.gofiber.io/application#group) - -```go -func main() { - app := fiber.New() - - // Root API route - api := app.Group("/api", cors()) // /api - - // API v1 routes - v1 := api.Group("/v1", mysql()) // /api/v1 - v1.Get("/list", handler) // /api/v1/list - v1.Get("/user", handler) // /api/v1/user - - // API v2 routes - v2 := api.Group("/v2", mongodb()) // /api/v2 - v2.Get("/list", handler) // /api/v2/list - v2.Get("/user", handler) // /api/v2/user - - // ... -} -``` - -### ไธญไป‹ๅ™จlogger - -๐Ÿ“– [Logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - // ้ ่จญ - app.Use(middleware.Logger()) - - // ๅฎขๅˆถๆ ผๅผ - app.Use(middleware.Logger("${method} - ${path}")) - - // ๅฎขๅˆถ่จญๅฎš - app.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ - Next: func(ctx *fiber.Ctx) bool { - return ctx.Path() != "/private" - }, - Format: "${method} - ${path}", - Output: io.Writer, - })) - - app.Listen(3000) -} - -### ่ทจ็ถฒๅŸŸ่ณ‡ๆบๅ…ฑไบซ (CORS) - -๐Ÿ“– [CORS](https://docs.gofiber.io/middleware#cors) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/cors" -) - -func main() { - app := fiber.New() - - // ้ ่จญ็š„CORS - app.Use(cors.New()) - - app.Listen(3000) -} -``` - -ๅœจ`Origin` headerไธญๆ”พ็ถฒๅŸŸไพ†ๆชขๆŸฅCORS: - -```bash -curl -H "Origin: http://example.com" --verbose http://localhost:3000 -``` - -### ๅฎขๅˆถ404ๅ›žๆ‡‰ - -๐Ÿ“– [HTTP Methods](https://docs.gofiber.io/application#http-methods) - -```go -func main() { - app := fiber.New() - - app.Static("./public") - - app.Get("/demo", func(c *fiber.Ctx) { - c.Send("This is a demo!") - }) - - app.Post("/register", func(c *fiber.Ctx) { - c.Send("Welcome!") - }) - - // ็”จๆœ€ๅพŒไธ€ๅ€‹ไธญไป‹ๅ™จๆ””ๆˆชๅ‰ฉ้ค˜็š„case - app.Use(func(c *fiber.Ctx) { - c.SendStatus(404) - // => 404 "Not Found" - }) - - app.Listen(3000) -} -``` - -### JSON ๅ›žๆ‡‰ - -๐Ÿ“– [JSON](https://docs.gofiber.io/context#json) - -```go -type User struct { - Name string `json:"name"` - Age int `json:"age"` -} - -func main() { - app := fiber.New() - - app.Get("/user", func(c *fiber.Ctx) { - c.JSON(&User{"John", 20}) - // => {"name":"John", "age":20} - }) - - app.Get("/json", func(c *fiber.Ctx) { - c.JSON(fiber.Map{ - "success": true, - "message": "Hi John!", - }) - // => {"success":true, "message":"Hi John!"} - }) - - app.Listen(3000) -} -``` - -### WebSocket ๅ‡็ดš - -๐Ÿ“– [Websocket](https://docs.gofiber.io/middleware#websocket) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/websocket" -) - -func main() { - app := fiber.New() - - app.Get("/ws", websocket.New(func(c *websocket.Conn) { - for { - mt, msg, err := c.ReadMessage() - if err != nil { - log.Println("read:", err) - break - } - log.Printf("recv: %s", msg) - err = c.WriteMessage(mt, msg) - if err != nil { - log.Println("write:", err) - break - } - } - })) - - app.Listen(3000) - // ws://localhost:3000/ws -} -``` - -### Recover ไธญไป‹ๅ™จ - -๐Ÿ“– [Recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) - -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - app := fiber.New() - - app.Use(middleware.Recover()) - - app.Get("/", func(c *fiber.Ctx) { - panic("normally this would crash your app") - }) - - app.Listen(3000) -} -``` -
- -## ๐Ÿงฌ Fiberไธญไป‹ๅ™จ - -ๅฎ˜ๆ–น[ๅœ˜้šŠ](https://github.com/orgs/gofiber/people)็ถญ่ญท็š„ไธญไป‹ๅ™จใ€‚ - -| ไธญไป‹ๅ™จ | ๆ•˜่ฟฐ | ๆ˜ฏๅฆๅ…งๅปบ | -| :--- | :--- | :--- | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | - | -| [basicauth](https://github.com/gofiber/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | - | -| [compress](https://github.com/gofiber/fiber/blob/master/middleware/compress.md) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | `middleware.Compress()` | -| [cors](https://github.com/gofiber/cors) | Enable cross-origin resource sharing \(CORS\) with various options. | - | -| [csrf](https://github.com/gofiber/csrf) | Protect from CSRF exploits. | - | -| [filesystem](https://github.com/gofiber/fiber/blob/master/middleware/filesystem.md) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | - | -| [favicon](https://github.com/gofiber/fiber/blob/master/middleware/favicon.md) | Ignore favicon from logs or serve from memory if a file path is provided. | `middleware.Favicon()` | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | - | -| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | - | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | - | -| [limiter](https://github.com/gofiber/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | - | -| [logger](https://github.com/gofiber/fiber/blob/master/middleware/logger.md) | HTTP request/response logger. | `middleware.Logger()` | -| [pprof](https://github.com/gofiber/pprof) | Special thanks to Matthew Lee \(@mthli\) | - | -| [recover](https://github.com/gofiber/fiber/blob/master/middleware/recover.md) | Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized[ ErrorHandler](error-handling.md). | `middleware.Recover()` | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | - | -| [requestid](https://github.com/gofiber/fiber/blob/master/middleware/request_id.md) | Request ID middleware generates a unique id for a request. | `middleware.RequestID()` | -| [session](https://github.com/gofiber/session) | This session middleware is build on top of fasthttp/session by @savsgio MIT. Special thanks to @thomasvvugt for helping with this middleware. | - | -| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | - | -| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | - | - -## ๐ŸŒฑ ็ฌฌไธ‰ๆ–นไธญไป‹ๅ™จ - -็”ฑ็คพ็พคๅปบ็ซ‹็š„ไธญไป‹ๅ™จๅˆ—่กจ๏ผŒ่ฆๆ–ฐๅขž่ซ‹็™ผPR! - -- [arsmn/fiber-casbin](https://github.com/arsmn/fiber-casbin) -- [arsmn/fiber-introspect](https://github.com/arsmn/fiber-introspect) -- [arsmn/fiber-swagger](https://github.com/arsmn/fiber-swagger) -- [arsmn/gqlgen](https://github.com/arsmn/gqlgen) -- [codemicro/fiber-cache](https://github.com/codemicro/fiber-cache) -- [itsursujit/fiber-boilerplate](https://github.com/itsursujit/fiber-boilerplate) -- [juandiii/go-jwk-security](https://github.com/juandiii/go-jwk-security) -- [kiyonlin/fiber_limiter](https://github.com/kiyonlin/fiber_limiter) -- [shareed2k/fiber_limiter](https://github.com/shareed2k/fiber_limiter) -- [shareed2k/fiber_tracing](https://github.com/shareed2k/fiber_tracing) -- [thomasvvugt/fiber-boilerplate](https://github.com/thomasvvugt/fiber-boilerplate) - -## ๐Ÿ’ฌ ๆ–ฐ่ž - -

- - -

- -- [Welcome to Fiber โ€” an Express.js styled web framework written in Go with โค๏ธ](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) โ€” _03 Feb 2020_ -- [Fiber released v1.7! ๐ŸŽ‰ What's new and is it still fast, flexible and friendly?](https://dev.to/koddr/fiber-v2-is-out-now-what-s-new-and-is-he-still-fast-flexible-and-friendly-3ipf) โ€” _21 Feb 2020_ -- [๐Ÿš€ Fiber v1.8. What's new, updated and re-thinked?](https://dev.to/koddr/fiber-v1-8-what-s-new-updated-and-re-thinked-339h) โ€” _03 Mar 2020_ -- [Is switching from Express to Fiber worth it? ๐Ÿค”](https://dev.to/koddr/are-sure-what-your-lovely-web-framework-running-so-fast-2jl1) โ€” _01 Apr 2020_ -- [Creating Fast APIs In Go Using Fiber](https://dev.to/jozsefsallai/creating-fast-apis-in-go-using-fiber-59m9) โ€” _07 Apr 2020_ -- [Building a Basic REST API in Go using Fiber](https://tutorialedge.net/golang/basic-rest-api-go-fiber/) - _23 Apr 2020_ -- [๐Ÿ“บ Building a REST API using GORM and Fiber](https://youtu.be/Iq2qT0fRhAA) - _25 Apr 2020_ -- [๐ŸŒŽ Create a travel list app with Go, Fiber, Angular, MongoDB and Google Cloud Secret Manager](https://blog.yongweilun.me/create-a-travel-list-app-with-go-fiber-angular-mongodb-and-google-cloud-secret-manager-ck9fgxy0p061pcss1xt1ubu8t) - _25 Apr 2020_ -- [Fiber v1.9.6 ๐Ÿ”ฅ How to improve performance by 817% and stay fast, flexible and friendly?](https://dev.to/koddr/fiber-v1-9-5-how-to-improve-performance-by-817-and-stay-fast-flexible-and-friendly-2dp6) - _12 May 2020_ -- [The road to web-based authentication with Fiber โšก](https://vugt.me/the-road-to-web-based-authentication-with-fiber/) - _20 May 2020_ -- [Building an Express-style API in Go with Fiber](https://blog.logrocket.com/express-style-api-go-fiber/) - _10 June 2020_ -- [ๅŸบไบŽgolang fiberๅ’Œangularๅผ€ๅ‘web](https://zhuanlan.zhihu.com/p/148925642) - _19 June 2020_ -- [ๅŸบไบŽๅปถ่ฟŸ่ฎก็ฎ—ไปค็‰Œๆกถ็š„gofiber้ข‘็Ž‡้™ๅˆถไธญ้—ดไปถๅฎž็Žฐ](https://zhuanlan.zhihu.com/p/149308936) - _20 June 2020_ -- [Construir una API en Golang con Fiber ๐Ÿ‡ช๐Ÿ‡ธ](https://enbonnet.me/article/53/construir-api-golang-con-fiber) - _28 June 2020_ -- [๐Ÿ“บWhy Go Fiber Is THE New Framework To Learn](https://www.youtube.com/watch?v=kvwsPeWDLM8) - _29 June 2020_ -- [่งฃๆžGofiber่ทฏ็”ฑ็ฎก็†](https://zhuanlan.zhihu.com/p/152494502) - _08 July 2020_ -- [๐Ÿ“บ Introduction to Fiber - An Express-inspired web framework](https://youtu.be/MfFi4Gt-tos) - _25 July 2020_ - -## ๐Ÿ‘ ่ฒข็ป - -ๅฆ‚ๆžœๆ‚จ่ฆ่ชช่ฒ**่ฌ่ฌ**ๆˆ–ๆ”ฏๆด`Fiber`็š„็ฉๆฅต็™ผๅฑ•๏ผš - -1. ้ปžๆ“Š[GitHub Star](https://github.com/gofiber/fiber/stargazers)้—œๆณจๆœฌๅฐˆๆกˆใ€‚ -2. ๅœจ[Twitter](https://twitter.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber)่ฝ‰[ๆŽจ](https://twitter.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber)ใ€‚ -3. ๅœจ[Medium](https://medium.com/)ใ€[Dev.to](https://dev.to/)ใ€้ƒจ่ฝๆ ผไธŠ็™ผ่กจๆ„่ฆ‹ๆˆ–ๆ•™ๅญธใ€‚ -4. ๅœจ[Crowdin](https://crowdin.com/project/gofiber) [![Crowdin](https://badges.crowdin.net/gofiber/localized.svg)](https://crowdin.com/project/gofiber)ๅนซๆˆ‘ๅ€‘็ฟป่ญฏAPIๆ–‡ไปถ -5. ่ดŠๅŠฉๆˆ‘ๅ€‘[ไธ€ๆฏๅ’–ๅ•ก](https://buymeacoff.ee/fenny)ใ€‚ - -## โ˜• ๆ”ฏๆŒ่€… - -Fiberๆ˜ฏไธ€ๅ€‹ไปฅ่ดŠๅŠฉ็ถญ็”Ÿ็š„้–‹ๆบๅฐˆๆกˆ๏ผŒๅƒๆ˜ฏ: ็ถฒๅŸŸใ€gitbookใ€netlifyใ€serverlessไผบๆœๅ™จใ€‚ๅฆ‚ๆžœไฝ ๆƒณ่ดŠๅŠฉไนŸๅฏไปฅโ˜• [**่ฒทๆฏๅ’–ๅ•ก**](https://buymeacoff.ee/fenny) - -| | User | Donation | -| :---------------------------------------------------------- | :----------------------------------------------- | :-------- | -| ![](https://avatars.githubusercontent.com/u/204341?s=25 ) | [@destari](https://github.com/destari) | โ˜• x 10 | -| ![](https://avatars.githubusercontent.com/u/63164982?s=25 ) | [@dembygenesis](https://github.com/dembygenesis) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/56607882?s=25 ) | [@thomasvvugt](https://github.com/thomasvvugt) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/27820675?s=25 ) | [@hendratommy](https://github.com/hendratommy) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/1094221?s=25 ) | [@ekaputra07](https://github.com/ekaputra07) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/194590?s=25 ) | [@jorgefuertes](https://github.com/jorgefuertes) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/186637?s=25 ) | [@candidosales](https://github.com/candidosales) | โ˜• x 5 | -| ![](https://avatars.githubusercontent.com/u/29659953?s=25 ) | [@l0nax](https://github.com/l0nax) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/59947262?s=25 ) | [@ankush](https://github.com/ankush) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/635852?s=25 ) | [@bihe](https://github.com/bihe) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/307334?s=25 ) | [@justdave](https://github.com/justdave) | โ˜• x 3 | -| ![](https://avatars.githubusercontent.com/u/11155743?s=25 ) | [@koddr](https://github.com/koddr) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/29042462?s=25 ) | [@lapolinar](https://github.com/lapolinar) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/2978730?s=25 ) | [@diegowifi](https://github.com/diegowifi) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/44171355?s=25 ) | [@ssimk0](https://github.com/ssimk0) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/5638101?s=25 ) | [@raymayemir](https://github.com/raymayemir) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/619996?s=25 ) | [@melkorm](https://github.com/melkorm) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31022056?s=25 ) | [@marvinjwendt](https://github.com/thomasvvugt) | โ˜• x 1 | -| ![](https://avatars.githubusercontent.com/u/31921460?s=25 ) | [@toishy](https://github.com/toishy) | โ˜• x 1 | - -## โ€Žโ€๐Ÿ’ป ่ฒข็ป่€… - -Code Contributors - -## โญ๏ธ Stargazers - -Stargazers over time - -## โš ๏ธ ๆŽˆๆฌŠ - -Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` is free and open-source software licensed under the [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Official logo was created by [Vic Shรณstak](https://github.com/koddr) and distributed under [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) license (CC BY-SA 4.0 International). - -**Third-party library licenses** -- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) -- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) -- [fasttemplate](https://github.com/valyala/fasttemplate/blob/master/LICENSE) -- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) diff --git a/.github/SECURITY.md b/.github/SECURITY.md deleted file mode 100644 index 30d08a5c..00000000 --- a/.github/SECURITY.md +++ /dev/null @@ -1,76 +0,0 @@ -# Security Policy - -1. [Supported Versions](#versions) -2. [Reporting security problems to Fiber](#reporting) -3. [Security Point of Contact](#contact) -4. [Incident Response Process](#process) - - -## Supported Versions - -The table below shows the supported versions for Fiber which include security updates. - -| Version | Supported | -| --------- | ------------------ | -| >= 1.12.6 | :white_check_mark: | -| < 1.12.6 | :x: | - - -## Reporting security problems to Fiber - -**DO NOT CREATE AN ISSUE** to report a security problem. Instead, please -send us an e-mail at `team@gofiber.io` or join our discord server via -[this invite link](https://discord.gg/bSnH7db) and send a private message -to Fenny or any of the maintainers. - - -## Security Point of Contact - -The security point of contact is [Fenny](https://github.com/Fenny). Fenny responds -to security incident reports as fast as possible, within one business day at the -latest. - -In case Fenny does not respond within a reasonable time, the secondary point -of contact are any of the [@maintainers](https://github.com/orgs/gofiber/teams/maintainers). -The maintainers are the only other persons with administrative access to Fiber's source code. - - -## Incident Response Process - -In case an incident is discovered or reported, we will follow the following -process to contain, respond and remediate: - -### 1. Containment - -The first step is to find out the root cause, nature and scope of the incident. - -- Is still ongoing? If yes, first priority is to stop it. -- Is the incident outside of our influence? If yes, first priority is to contain it. -- Find out knows about the incident and who is affected. -- Find out what data was potentially exposed. - -### 2. Response - -After the initial assessment and containment to our best abilities, we will -document all actions taken in a response plan. - -We will create a comment in the official `#announcements` channel to inform users about -the incident and what actions we took to contain it. - -### 3. Remediation - -Once the incident is confirmed to be resolved, we will summarize the lessons -learned from the incident and create a list of actions we will take to prevent -it from happening again. - -### Secure accounts with access - -The [Fiber Organization](https://github.com/gofiber) requires 2FA authorization -for all of it's members. - -### Critical Updates And Security Notices - -We learn about critical software updates and security threats from these sources - -1. GitHub Security Alerts -2. GitHub: https://status.github.com/ & [@githubstatus](https://twitter.com/githubstatus) diff --git a/.github/TEST_DATA/index.tmpl b/.github/TEST_DATA/index.tmpl deleted file mode 100644 index 131044d7..00000000 --- a/.github/TEST_DATA/index.tmpl +++ /dev/null @@ -1 +0,0 @@ -

{{.Title}}

\ No newline at end of file diff --git a/.github/TEST_DATA/ssl.key b/.github/TEST_DATA/ssl.key deleted file mode 100644 index cd9e2515..00000000 --- a/.github/TEST_DATA/ssl.key +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD4IQusAs8PJdnG -3mURt/AXtgC+ceqLOatJ49JJE1VPTkMAy+oE1f1XvkMrYsHqmDf6GWVzgVXryL4U -wq2/nJSm56ddhN55nI8oSN3dtywUB8/ShelEN73nlN77PeD9tl6NksPwWaKrqxq0 -FlabRPZSQCfmgZbhDV8Sa8mfCkFU0G0lit6kLGceCKMvmW+9Bz7ebsYmVdmVMxmf -IJStFD44lWFTdUc65WISKEdW2ELcUefb0zOLw+0PCbXFGJH5x5ktksW8+BBk2Hkg -GeQRL/qPCccthbScO0VgNj3zJ3ZZL0ObSDAbvNDG85joeNjDNq5DT/BAZ0bOSbEF -sh+f9BAzAgMBAAECggEBAJWv2cq7Jw6MVwSRxYca38xuD6TUNBopgBvjREixURW2 -sNUaLuMb9Omp7fuOaE2N5rcJ+xnjPGIxh/oeN5MQctz9gwn3zf6vY+15h97pUb4D -uGvYPRDaT8YVGS+X9NMZ4ZCmqW2lpWzKnCFoGHcy8yZLbcaxBsRdvKzwOYGoPiFb -K2QuhXZ/1UPmqK9i2DFKtj40X6vBszTNboFxOVpXrPu0FJwLVSDf2hSZ4fMM0DH3 -YqwKcYf5te+hxGKgrqRA3tn0NCWii0in6QIwXMC+kMw1ebg/tZKqyDLMNptAK8J+ -DVw9m5X1seUHS5ehU/g2jrQrtK5WYn7MrFK4lBzlRwECgYEA/d1TeANYECDWRRDk -B0aaRZs87Rwl/J9PsvbsKvtU/bX+OfSOUjOa9iQBqn0LmU8GqusEET/QVUfocVwV -Bggf/5qDLxz100Rj0ags/yE/kNr0Bb31kkkKHFMnCT06YasR7qKllwrAlPJvQv9x -IzBKq+T/Dx08Wep9bCRSFhzRCnsCgYEA+jdeZXTDr/Vz+D2B3nAw1frqYFfGnEVY -wqmoK3VXMDkGuxsloO2rN+SyiUo3JNiQNPDub/t7175GH5pmKtZOlftePANsUjBj -wZ1D0rI5Bxu/71ibIUYIRVmXsTEQkh/ozoh3jXCZ9+bLgYiYx7789IUZZSokFQ3D -FICUT9KJ36kCgYAGoq9Y1rWJjmIrYfqj2guUQC+CfxbbGIrrwZqAsRsSmpwvhZ3m -tiSZxG0quKQB+NfSxdvQW5ulbwC7Xc3K35F+i9pb8+TVBdeaFkw+yu6vaZmxQLrX -fQM/pEjD7A7HmMIaO7QaU5SfEAsqdCTP56Y8AftMuNXn/8IRfo2KuGwaWwKBgFpU -ILzJoVdlad9E/Rw7LjYhZfkv1uBVXIyxyKcfrkEXZSmozDXDdxsvcZCEfVHM6Ipk -K/+7LuMcqp4AFEAEq8wTOdq6daFaHLkpt/FZK6M4TlruhtpFOPkoNc3e45eM83OT -6mziKINJC1CQ6m65sQHpBtjxlKMRG8rL/D6wx9s5AoGBAMRlqNPMwglT3hvDmsAt -9Lf9pdmhERUlHhD8bj8mDaBj2Aqv7f6VRJaYZqP403pKKQexuqcn80mtjkSAPFkN -Cj7BVt/RXm5uoxDTnfi26RF9F6yNDEJ7UU9+peBr99aazF/fTgW/1GcMkQnum8uV -c257YgaWmjK9uB0Y2r2VxS0G ------END PRIVATE KEY----- \ No newline at end of file diff --git a/.github/TEST_DATA/ssl.pem b/.github/TEST_DATA/ssl.pem deleted file mode 100644 index 4cf41c9e..00000000 --- a/.github/TEST_DATA/ssl.pem +++ /dev/null @@ -1,17 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICujCCAaKgAwIBAgIJAMbXnKZ/cikUMA0GCSqGSIb3DQEBCwUAMBUxEzARBgNV -BAMTCnVidW50dS5uYW4wHhcNMTUwMjA0MDgwMTM5WhcNMjUwMjAxMDgwMTM5WjAV -MRMwEQYDVQQDEwp1YnVudHUubmFuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB -CgKCAQEA+CELrALPDyXZxt5lEbfwF7YAvnHqizmrSePSSRNVT05DAMvqBNX9V75D -K2LB6pg3+hllc4FV68i+FMKtv5yUpuenXYTeeZyPKEjd3bcsFAfP0oXpRDe955Te -+z3g/bZejZLD8Fmiq6satBZWm0T2UkAn5oGW4Q1fEmvJnwpBVNBtJYrepCxnHgij -L5lvvQc+3m7GJlXZlTMZnyCUrRQ+OJVhU3VHOuViEihHVthC3FHn29Mzi8PtDwm1 -xRiR+ceZLZLFvPgQZNh5IBnkES/6jwnHLYW0nDtFYDY98yd2WS9Dm0gwG7zQxvOY -6HjYwzauQ0/wQGdGzkmxBbIfn/QQMwIDAQABow0wCzAJBgNVHRMEAjAAMA0GCSqG -SIb3DQEBCwUAA4IBAQBQjKm/4KN/iTgXbLTL3i7zaxYXFLXsnT1tF+ay4VA8aj98 -L3JwRTciZ3A5iy/W4VSCt3eASwOaPWHKqDBB5RTtL73LoAqsWmO3APOGQAbixcQ2 -45GXi05OKeyiYRi1Nvq7Unv9jUkRDHUYVPZVSAjCpsXzPhFkmZoTRxmx5l0ZF7Li -K91lI5h+eFq0dwZwrmlPambyh1vQUi70VHv8DNToVU29kel7YLbxGbuqETfhrcy6 -X+Mha6RYITkAn5FqsZcKMsc9eYGEF4l3XV+oS7q6xfTxktYJMFTI18J0lQ2Lv/CI -whdMnYGntDQBE/iFCrJEGNsKGc38796GBOb5j+zd ------END CERTIFICATE----- \ No newline at end of file diff --git a/.github/TEST_DATA/template-invalid.html b/.github/TEST_DATA/template-invalid.html deleted file mode 100644 index ac4f6d13..00000000 --- a/.github/TEST_DATA/template-invalid.html +++ /dev/null @@ -1 +0,0 @@ -

{{.Title}

diff --git a/.github/TEST_DATA/template.html b/.github/TEST_DATA/template.html deleted file mode 100644 index 131044d7..00000000 --- a/.github/TEST_DATA/template.html +++ /dev/null @@ -1 +0,0 @@ -

{{.Title}}

\ No newline at end of file diff --git a/.github/TEST_DATA/testRoutes.json b/.github/TEST_DATA/testRoutes.json deleted file mode 100644 index 8e08aef1..00000000 --- a/.github/TEST_DATA/testRoutes.json +++ /dev/null @@ -1,1918 +0,0 @@ -{ - "testRoutes": [ - { - "method": "GET", - "path": "/authorizations" - }, - { - "method": "GET", - "path": "/authorizations/1337" - }, - { - "method": "POST", - "path": "/authorizations" - }, - { - "method": "PUT", - "path": "/authorizations/clients/inf1nd873nf8912g9t" - }, - { - "method": "PATCH", - "path": "/authorizations/1337" - }, - { - "method": "DELETE", - "path": "/authorizations/1337" - }, - { - "method": "GET", - "path": "/applications/2nds981mng6azl127y/tokens/sn108hbe1geheibf13f" - }, - { - "method": "DELETE", - "path": "/applications/2nds981mng6azl127y/tokens" - }, - { - "method": "DELETE", - "path": "/applications/2nds981mng6azl127y/tokens/sn108hbe1geheibf13f" - }, - { - "method": "GET", - "path": "/events" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/events" - }, - { - "method": "GET", - "path": "/networks/fenny/fiber/events" - }, - { - "method": "GET", - "path": "/orgs/gofiber/events" - }, - { - "method": "GET", - "path": "/users/fenny/received_events" - }, - { - "method": "GET", - "path": "/users/fenny/received_events/public" - }, - { - "method": "GET", - "path": "/users/fenny/events" - }, - { - "method": "GET", - "path": "/users/fenny/events/public" - }, - { - "method": "GET", - "path": "/users/fenny/events/orgs/gofiber" - }, - { - "method": "GET", - "path": "/feeds" - }, - { - "method": "GET", - "path": "/notifications" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/notifications" - }, - { - "method": "PUT", - "path": "/notifications" - }, - { - "method": "PUT", - "path": "/repos/fenny/fiber/notifications" - }, - { - "method": "GET", - "path": "/notifications/threads/1337" - }, - { - "method": "PATCH", - "path": "/notifications/threads/1337" - }, - { - "method": "GET", - "path": "/notifications/threads/1337/subscription" - }, - { - "method": "PUT", - "path": "/notifications/threads/1337/subscription" - }, - { - "method": "DELETE", - "path": "/notifications/threads/1337/subscription" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/stargazers" - }, - { - "method": "GET", - "path": "/users/fenny/starred" - }, - { - "method": "GET", - "path": "/user/starred" - }, - { - "method": "GET", - "path": "/user/starred/fenny/fiber" - }, - { - "method": "PUT", - "path": "/user/starred/fenny/fiber" - }, - { - "method": "DELETE", - "path": "/user/starred/fenny/fiber" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/subscribers" - }, - { - "method": "GET", - "path": "/users/fenny/subscriptions" - }, - { - "method": "GET", - "path": "/user/subscriptions" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/subscription" - }, - { - "method": "PUT", - "path": "/repos/fenny/fiber/subscription" - }, - { - "method": "DELETE", - "path": "/repos/fenny/fiber/subscription" - }, - { - "method": "GET", - "path": "/user/subscriptions/fenny/fiber" - }, - { - "method": "PUT", - "path": "/user/subscriptions/fenny/fiber" - }, - { - "method": "DELETE", - "path": "/user/subscriptions/fenny/fiber" - }, - { - "method": "GET", - "path": "/users/fenny/gists" - }, - { - "method": "GET", - "path": "/gists" - }, - { - "method": "GET", - "path": "/gists/public" - }, - { - "method": "GET", - "path": "/gists/starred" - }, - { - "method": "GET", - "path": "/gists/1337" - }, - { - "method": "POST", - "path": "/gists" - }, - { - "method": "PATCH", - "path": "/gists/1337" - }, - { - "method": "PUT", - "path": "/gists/1337/star" - }, - { - "method": "DELETE", - "path": "/gists/1337/star" - }, - { - "method": "GET", - "path": "/gists/1337/star" - }, - { - "method": "POST", - "path": "/gists/1337/forks" - }, - { - "method": "DELETE", - "path": "/gists/1337" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/git/blobs/v948b24g98ubngw9082bn02giub" - }, - { - "method": "POST", - "path": "/repos/fenny/fiber/git/blobs" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/git/commits/v948b24g98ubngw9082bn02giub" - }, - { - "method": "POST", - "path": "/repos/fenny/fiber/git/commits" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/git/refs/im/a/wildcard" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/git/refs" - }, - { - "method": "POST", - "path": "/repos/fenny/fiber/git/refs" - }, - { - "method": "PATCH", - "path": "/repos/fenny/fiber/git/refs/im/a/wildcard" - }, - { - "method": "DELETE", - "path": "/repos/fenny/fiber/git/refs/im/a/wildcard" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/git/tags/v948b24g98ubngw9082bn02giub" - }, - { - "method": "POST", - "path": "/repos/fenny/fiber/git/tags" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/git/trees/v948b24g98ubngw9082bn02giub" - }, - { - "method": "POST", - "path": "/repos/fenny/fiber/git/trees" - }, - { - "method": "GET", - "path": "/issues" - }, - { - "method": "GET", - "path": "/user/issues" - }, - { - "method": "GET", - "path": "/orgs/gofiber/issues" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/issues" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/issues/1000" - }, - { - "method": "POST", - "path": "/repos/fenny/fiber/issues" - }, - { - "method": "PATCH", - "path": "/repos/fenny/fiber/issues/1000" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/assignees" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/assignees/nic" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/issues/1000/comments" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/issues/comments" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/issues/comments/1337" - }, - { - "method": "POST", - "path": "/repos/fenny/fiber/issues/1000/comments" - }, - { - "method": "PATCH", - "path": "/repos/fenny/fiber/issues/comments/1337" - }, - { - "method": "DELETE", - "path": "/repos/fenny/fiber/issues/comments/1337" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/issues/1000/events" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/issues/events" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/issues/events/1337" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/labels" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/labels/john" - }, - { - "method": "POST", - "path": "/repos/fenny/fiber/labels" - }, - { - "method": "PATCH", - "path": "/repos/fenny/fiber/labels/john" - }, - { - "method": "DELETE", - "path": "/repos/fenny/fiber/labels/john" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/issues/1000/labels" - }, - { - "method": "POST", - "path": "/repos/fenny/fiber/issues/1000/labels" - }, - { - "method": "DELETE", - "path": "/repos/fenny/fiber/issues/1000/labels/john" - }, - { - "method": "PUT", - "path": "/repos/fenny/fiber/issues/1000/labels" - }, - { - "method": "DELETE", - "path": "/repos/fenny/fiber/issues/1000/labels" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/milestones/1000/labels" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/milestones" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/milestones/1000" - }, - { - "method": "POST", - "path": "/repos/fenny/fiber/milestones" - }, - { - "method": "PATCH", - "path": "/repos/fenny/fiber/milestones/1000" - }, - { - "method": "DELETE", - "path": "/repos/fenny/fiber/milestones/1000" - }, - { - "method": "GET", - "path": "/emojis" - }, - { - "method": "GET", - "path": "/gitignore/templates" - }, - { - "method": "GET", - "path": "/gitignore/templates/john" - }, - { - "method": "POST", - "path": "/markdown" - }, - { - "method": "POST", - "path": "/markdown/raw" - }, - { - "method": "GET", - "path": "/meta" - }, - { - "method": "GET", - "path": "/rate_limit" - }, - { - "method": "GET", - "path": "/users/fenny/orgs" - }, - { - "method": "GET", - "path": "/user/orgs" - }, - { - "method": "GET", - "path": "/orgs/gofiber" - }, - { - "method": "PATCH", - "path": "/orgs/gofiber" - }, - { - "method": "GET", - "path": "/orgs/gofiber/members" - }, - { - "method": "GET", - "path": "/orgs/gofiber/members/fenny" - }, - { - "method": "DELETE", - "path": "/orgs/gofiber/members/fenny" - }, - { - "method": "GET", - "path": "/orgs/gofiber/public_members" - }, - { - "method": "GET", - "path": "/orgs/gofiber/public_members/fenny" - }, - { - "method": "PUT", - "path": "/orgs/gofiber/public_members/fenny" - }, - { - "method": "DELETE", - "path": "/orgs/gofiber/public_members/fenny" - }, - { - "method": "GET", - "path": "/orgs/gofiber/teams" - }, - { - "method": "GET", - "path": "/teams/1337" - }, - { - "method": "POST", - "path": "/orgs/gofiber/teams" - }, - { - "method": "PATCH", - "path": "/teams/1337" - }, - { - "method": "DELETE", - "path": "/teams/1337" - }, - { - "method": "GET", - "path": "/teams/1337/members" - }, - { - "method": "GET", - "path": "/teams/1337/members/fenny" - }, - { - "method": "PUT", - "path": "/teams/1337/members/fenny" - }, - { - "method": "DELETE", - "path": "/teams/1337/members/fenny" - }, - { - "method": "GET", - "path": "/teams/1337/repos" - }, - { - "method": "GET", - "path": "/teams/1337/repos/fenny/fiber" - }, - { - "method": "PUT", - "path": "/teams/1337/repos/fenny/fiber" - }, - { - "method": "DELETE", - "path": "/teams/1337/repos/fenny/fiber" - }, - { - "method": "GET", - "path": "/user/teams" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/pulls" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/pulls/1000" - }, - { - "method": "POST", - "path": "/repos/fenny/fiber/pulls" - }, - { - "method": "PATCH", - "path": "/repos/fenny/fiber/pulls/1000" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/pulls/1000/commits" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/pulls/1000/files" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/pulls/1000/merge" - }, - { - "method": "PUT", - "path": "/repos/fenny/fiber/pulls/1000/merge" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/pulls/1000/comments" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/pulls/comments" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/pulls/comments/1000" - }, - { - "method": "PUT", - "path": "/repos/fenny/fiber/pulls/1000/comments" - }, - { - "method": "PATCH", - "path": "/repos/fenny/fiber/pulls/comments/1000" - }, - { - "method": "DELETE", - "path": "/repos/fenny/fiber/pulls/comments/1000" - }, - { - "method": "GET", - "path": "/user/repos" - }, - { - "method": "GET", - "path": "/users/fenny/repos" - }, - { - "method": "GET", - "path": "/orgs/gofiber/repos" - }, - { - "method": "GET", - "path": "/repositories" - }, - { - "method": "POST", - "path": "/user/repos" - }, - { - "method": "POST", - "path": "/orgs/gofiber/repos" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber" - }, - { - "method": "PATCH", - "path": "/repos/fenny/fiber" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/contributors" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/languages" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/teams" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/tags" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/branches" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/branches/master" - }, - { - "method": "DELETE", - "path": "/repos/fenny/fiber" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/collaborators" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/collaborators/fenny" - }, - { - "method": "PUT", - "path": "/repos/fenny/fiber/collaborators/fenny" - }, - { - "method": "DELETE", - "path": "/repos/fenny/fiber/collaborators/fenny" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/comments" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/commits/v948b24g98ubngw9082bn02giub/comments" - }, - { - "method": "POST", - "path": "/repos/fenny/fiber/commits/v948b24g98ubngw9082bn02giub/comments" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/comments/1337" - }, - { - "method": "PATCH", - "path": "/repos/fenny/fiber/comments/1337" - }, - { - "method": "DELETE", - "path": "/repos/fenny/fiber/comments/1337" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/commits" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/commits/v948b24g98ubngw9082bn02giub" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/readme" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/contents/im/a/wildcard" - }, - { - "method": "PUT", - "path": "/repos/fenny/fiber/contents/im/a/wildcard" - }, - { - "method": "DELETE", - "path": "/repos/fenny/fiber/contents/im/a/wildcard" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/gzip/google" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/keys" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/keys/1337" - }, - { - "method": "POST", - "path": "/repos/fenny/fiber/keys" - }, - { - "method": "PATCH", - "path": "/repos/fenny/fiber/keys/1337" - }, - { - "method": "DELETE", - "path": "/repos/fenny/fiber/keys/1337" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/downloads" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/downloads/1337" - }, - { - "method": "DELETE", - "path": "/repos/fenny/fiber/downloads/1337" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/forks" - }, - { - "method": "POST", - "path": "/repos/fenny/fiber/forks" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/hooks" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/hooks/1337" - }, - { - "method": "POST", - "path": "/repos/fenny/fiber/hooks" - }, - { - "method": "PATCH", - "path": "/repos/fenny/fiber/hooks/1337" - }, - { - "method": "POST", - "path": "/repos/fenny/fiber/hooks/1337/tests" - }, - { - "method": "DELETE", - "path": "/repos/fenny/fiber/hooks/1337" - }, - { - "method": "POST", - "path": "/repos/fenny/fiber/merges" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/releases" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/releases/1337" - }, - { - "method": "POST", - "path": "/repos/fenny/fiber/releases" - }, - { - "method": "PATCH", - "path": "/repos/fenny/fiber/releases/1337" - }, - { - "method": "DELETE", - "path": "/repos/fenny/fiber/releases/1337" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/releases/1337/assets" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/stats/contributors" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/stats/commit_activity" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/stats/code_frequency" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/stats/participation" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/stats/punch_card" - }, - { - "method": "GET", - "path": "/repos/fenny/fiber/statuses/google" - }, - { - "method": "POST", - "path": "/repos/fenny/fiber/statuses/google" - }, - { - "method": "GET", - "path": "/search/repositories" - }, - { - "method": "GET", - "path": "/search/code" - }, - { - "method": "GET", - "path": "/search/issues" - }, - { - "method": "GET", - "path": "/search/users" - }, - { - "method": "GET", - "path": "/legacy/issues/search/fenny/fibersitory/locked/finish" - }, - { - "method": "GET", - "path": "/legacy/repos/search/finish" - }, - { - "method": "GET", - "path": "/legacy/user/search/finish" - }, - { - "method": "GET", - "path": "/legacy/user/email/info@gofiber.io" - }, - { - "method": "GET", - "path": "/users/fenny" - }, - { - "method": "GET", - "path": "/user" - }, - { - "method": "PATCH", - "path": "/user" - }, - { - "method": "GET", - "path": "/users" - }, - { - "method": "GET", - "path": "/user/emails" - }, - { - "method": "POST", - "path": "/user/emails" - }, - { - "method": "DELETE", - "path": "/user/emails" - }, - { - "method": "GET", - "path": "/users/fenny/followers" - }, - { - "method": "GET", - "path": "/user/followers" - }, - { - "method": "GET", - "path": "/users/fenny/following" - }, - { - "method": "GET", - "path": "/user/following" - }, - { - "method": "GET", - "path": "/user/following/fenny" - }, - { - "method": "GET", - "path": "/users/fenny/following/renan" - }, - { - "method": "PUT", - "path": "/user/following/fenny" - }, - { - "method": "DELETE", - "path": "/user/following/fenny" - }, - { - "method": "GET", - "path": "/users/fenny/keys" - }, - { - "method": "GET", - "path": "/user/keys" - }, - { - "method": "GET", - "path": "/user/keys/1337" - }, - { - "method": "POST", - "path": "/user/keys" - }, - { - "method": "PATCH", - "path": "/user/keys/1337" - }, - { - "method": "DELETE", - "path": "/user/keys/1337" - } - ], - "githubAPI": [ - { - "method": "GET", - "path": "/authorizations" - }, - { - "method": "GET", - "path": "/authorizations/:id" - }, - { - "method": "POST", - "path": "/authorizations" - }, - { - "method": "PUT", - "path": "/authorizations/clients/:client_id" - }, - { - "method": "PATCH", - "path": "/authorizations/:id" - }, - { - "method": "DELETE", - "path": "/authorizations/:id" - }, - { - "method": "GET", - "path": "/applications/:client_id/tokens/:access_token" - }, - { - "method": "DELETE", - "path": "/applications/:client_id/tokens" - }, - { - "method": "DELETE", - "path": "/applications/:client_id/tokens/:access_token" - }, - { - "method": "GET", - "path": "/events" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/events" - }, - { - "method": "GET", - "path": "/networks/:owner/:repo/events" - }, - { - "method": "GET", - "path": "/orgs/:org/events" - }, - { - "method": "GET", - "path": "/users/:user/received_events" - }, - { - "method": "GET", - "path": "/users/:user/received_events/public" - }, - { - "method": "GET", - "path": "/users/:user/events" - }, - { - "method": "GET", - "path": "/users/:user/events/public" - }, - { - "method": "GET", - "path": "/users/:user/events/orgs/:org" - }, - { - "method": "GET", - "path": "/feeds" - }, - { - "method": "GET", - "path": "/notifications" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/notifications" - }, - { - "method": "PUT", - "path": "/notifications" - }, - { - "method": "PUT", - "path": "/repos/:owner/:repo/notifications" - }, - { - "method": "GET", - "path": "/notifications/threads/:id" - }, - { - "method": "PATCH", - "path": "/notifications/threads/:id" - }, - { - "method": "GET", - "path": "/notifications/threads/:id/subscription" - }, - { - "method": "PUT", - "path": "/notifications/threads/:id/subscription" - }, - { - "method": "DELETE", - "path": "/notifications/threads/:id/subscription" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/stargazers" - }, - { - "method": "GET", - "path": "/users/:user/starred" - }, - { - "method": "GET", - "path": "/user/starred" - }, - { - "method": "GET", - "path": "/user/starred/:owner/:repo" - }, - { - "method": "PUT", - "path": "/user/starred/:owner/:repo" - }, - { - "method": "DELETE", - "path": "/user/starred/:owner/:repo" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/subscribers" - }, - { - "method": "GET", - "path": "/users/:user/subscriptions" - }, - { - "method": "GET", - "path": "/user/subscriptions" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/subscription" - }, - { - "method": "PUT", - "path": "/repos/:owner/:repo/subscription" - }, - { - "method": "DELETE", - "path": "/repos/:owner/:repo/subscription" - }, - { - "method": "GET", - "path": "/user/subscriptions/:owner/:repo" - }, - { - "method": "PUT", - "path": "/user/subscriptions/:owner/:repo" - }, - { - "method": "DELETE", - "path": "/user/subscriptions/:owner/:repo" - }, - { - "method": "GET", - "path": "/users/:user/gists" - }, - { - "method": "GET", - "path": "/gists" - }, - { - "method": "GET", - "path": "/gists/public" - }, - { - "method": "GET", - "path": "/gists/starred" - }, - { - "method": "GET", - "path": "/gists/:id" - }, - { - "method": "POST", - "path": "/gists" - }, - { - "method": "PATCH", - "path": "/gists/:id" - }, - { - "method": "PUT", - "path": "/gists/:id/star" - }, - { - "method": "DELETE", - "path": "/gists/:id/star" - }, - { - "method": "GET", - "path": "/gists/:id/star" - }, - { - "method": "POST", - "path": "/gists/:id/forks" - }, - { - "method": "DELETE", - "path": "/gists/:id" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/git/blobs/:sha" - }, - { - "method": "POST", - "path": "/repos/:owner/:repo/git/blobs" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/git/commits/:sha" - }, - { - "method": "POST", - "path": "/repos/:owner/:repo/git/commits" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/git/refs/*" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/git/refs" - }, - { - "method": "POST", - "path": "/repos/:owner/:repo/git/refs" - }, - { - "method": "PATCH", - "path": "/repos/:owner/:repo/git/refs/*" - }, - { - "method": "DELETE", - "path": "/repos/:owner/:repo/git/refs/*" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/git/tags/:sha" - }, - { - "method": "POST", - "path": "/repos/:owner/:repo/git/tags" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/git/trees/:sha" - }, - { - "method": "POST", - "path": "/repos/:owner/:repo/git/trees" - }, - { - "method": "GET", - "path": "/issues" - }, - { - "method": "GET", - "path": "/user/issues" - }, - { - "method": "GET", - "path": "/orgs/:org/issues" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/issues" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/issues/:number" - }, - { - "method": "POST", - "path": "/repos/:owner/:repo/issues" - }, - { - "method": "PATCH", - "path": "/repos/:owner/:repo/issues/:number" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/assignees" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/assignees/:assignee" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/issues/:number/comments" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/issues/comments" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/issues/comments/:id" - }, - { - "method": "POST", - "path": "/repos/:owner/:repo/issues/:number/comments" - }, - { - "method": "PATCH", - "path": "/repos/:owner/:repo/issues/comments/:id" - }, - { - "method": "DELETE", - "path": "/repos/:owner/:repo/issues/comments/:id" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/issues/:number/events" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/issues/events" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/issues/events/:id" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/labels" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/labels/:name" - }, - { - "method": "POST", - "path": "/repos/:owner/:repo/labels" - }, - { - "method": "PATCH", - "path": "/repos/:owner/:repo/labels/:name" - }, - { - "method": "DELETE", - "path": "/repos/:owner/:repo/labels/:name" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/issues/:number/labels" - }, - { - "method": "POST", - "path": "/repos/:owner/:repo/issues/:number/labels" - }, - { - "method": "DELETE", - "path": "/repos/:owner/:repo/issues/:number/labels/:name" - }, - { - "method": "PUT", - "path": "/repos/:owner/:repo/issues/:number/labels" - }, - { - "method": "DELETE", - "path": "/repos/:owner/:repo/issues/:number/labels" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/milestones/:number/labels" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/milestones" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/milestones/:number" - }, - { - "method": "POST", - "path": "/repos/:owner/:repo/milestones" - }, - { - "method": "PATCH", - "path": "/repos/:owner/:repo/milestones/:number" - }, - { - "method": "DELETE", - "path": "/repos/:owner/:repo/milestones/:number" - }, - { - "method": "GET", - "path": "/emojis" - }, - { - "method": "GET", - "path": "/gitignore/templates" - }, - { - "method": "GET", - "path": "/gitignore/templates/:name" - }, - { - "method": "POST", - "path": "/markdown" - }, - { - "method": "POST", - "path": "/markdown/raw" - }, - { - "method": "GET", - "path": "/meta" - }, - { - "method": "GET", - "path": "/rate_limit" - }, - { - "method": "GET", - "path": "/users/:user/orgs" - }, - { - "method": "GET", - "path": "/user/orgs" - }, - { - "method": "GET", - "path": "/orgs/:org" - }, - { - "method": "PATCH", - "path": "/orgs/:org" - }, - { - "method": "GET", - "path": "/orgs/:org/members" - }, - { - "method": "GET", - "path": "/orgs/:org/members/:user" - }, - { - "method": "DELETE", - "path": "/orgs/:org/members/:user" - }, - { - "method": "GET", - "path": "/orgs/:org/public_members" - }, - { - "method": "GET", - "path": "/orgs/:org/public_members/:user" - }, - { - "method": "PUT", - "path": "/orgs/:org/public_members/:user" - }, - { - "method": "DELETE", - "path": "/orgs/:org/public_members/:user" - }, - { - "method": "GET", - "path": "/orgs/:org/teams" - }, - { - "method": "GET", - "path": "/teams/:id" - }, - { - "method": "POST", - "path": "/orgs/:org/teams" - }, - { - "method": "PATCH", - "path": "/teams/:id" - }, - { - "method": "DELETE", - "path": "/teams/:id" - }, - { - "method": "GET", - "path": "/teams/:id/members" - }, - { - "method": "GET", - "path": "/teams/:id/members/:user" - }, - { - "method": "PUT", - "path": "/teams/:id/members/:user" - }, - { - "method": "DELETE", - "path": "/teams/:id/members/:user" - }, - { - "method": "GET", - "path": "/teams/:id/repos" - }, - { - "method": "GET", - "path": "/teams/:id/repos/:owner/:repo" - }, - { - "method": "PUT", - "path": "/teams/:id/repos/:owner/:repo" - }, - { - "method": "DELETE", - "path": "/teams/:id/repos/:owner/:repo" - }, - { - "method": "GET", - "path": "/user/teams" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/pulls" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/pulls/:number" - }, - { - "method": "POST", - "path": "/repos/:owner/:repo/pulls" - }, - { - "method": "PATCH", - "path": "/repos/:owner/:repo/pulls/:number" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/pulls/:number/commits" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/pulls/:number/files" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/pulls/:number/merge" - }, - { - "method": "PUT", - "path": "/repos/:owner/:repo/pulls/:number/merge" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/pulls/:number/comments" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/pulls/comments" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/pulls/comments/:number" - }, - { - "method": "PUT", - "path": "/repos/:owner/:repo/pulls/:number/comments" - }, - { - "method": "PATCH", - "path": "/repos/:owner/:repo/pulls/comments/:number" - }, - { - "method": "DELETE", - "path": "/repos/:owner/:repo/pulls/comments/:number" - }, - { - "method": "GET", - "path": "/user/repos" - }, - { - "method": "GET", - "path": "/users/:user/repos" - }, - { - "method": "GET", - "path": "/orgs/:org/repos" - }, - { - "method": "GET", - "path": "/repositories" - }, - { - "method": "POST", - "path": "/user/repos" - }, - { - "method": "POST", - "path": "/orgs/:org/repos" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo" - }, - { - "method": "PATCH", - "path": "/repos/:owner/:repo" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/contributors" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/languages" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/teams" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/tags" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/branches" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/branches/:branch" - }, - { - "method": "DELETE", - "path": "/repos/:owner/:repo" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/collaborators" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/collaborators/:user" - }, - { - "method": "PUT", - "path": "/repos/:owner/:repo/collaborators/:user" - }, - { - "method": "DELETE", - "path": "/repos/:owner/:repo/collaborators/:user" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/comments" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/commits/:sha/comments" - }, - { - "method": "POST", - "path": "/repos/:owner/:repo/commits/:sha/comments" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/comments/:id" - }, - { - "method": "PATCH", - "path": "/repos/:owner/:repo/comments/:id" - }, - { - "method": "DELETE", - "path": "/repos/:owner/:repo/comments/:id" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/commits" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/commits/:sha" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/readme" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/contents/*" - }, - { - "method": "PUT", - "path": "/repos/:owner/:repo/contents/*" - }, - { - "method": "DELETE", - "path": "/repos/:owner/:repo/contents/*" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/:archive_format/:ref" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/keys" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/keys/:id" - }, - { - "method": "POST", - "path": "/repos/:owner/:repo/keys" - }, - { - "method": "PATCH", - "path": "/repos/:owner/:repo/keys/:id" - }, - { - "method": "DELETE", - "path": "/repos/:owner/:repo/keys/:id" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/downloads" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/downloads/:id" - }, - { - "method": "DELETE", - "path": "/repos/:owner/:repo/downloads/:id" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/forks" - }, - { - "method": "POST", - "path": "/repos/:owner/:repo/forks" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/hooks" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/hooks/:id" - }, - { - "method": "POST", - "path": "/repos/:owner/:repo/hooks" - }, - { - "method": "PATCH", - "path": "/repos/:owner/:repo/hooks/:id" - }, - { - "method": "POST", - "path": "/repos/:owner/:repo/hooks/:id/tests" - }, - { - "method": "DELETE", - "path": "/repos/:owner/:repo/hooks/:id" - }, - { - "method": "POST", - "path": "/repos/:owner/:repo/merges" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/releases" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/releases/:id" - }, - { - "method": "POST", - "path": "/repos/:owner/:repo/releases" - }, - { - "method": "PATCH", - "path": "/repos/:owner/:repo/releases/:id" - }, - { - "method": "DELETE", - "path": "/repos/:owner/:repo/releases/:id" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/releases/:id/assets" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/stats/contributors" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/stats/commit_activity" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/stats/code_frequency" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/stats/participation" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/stats/punch_card" - }, - { - "method": "GET", - "path": "/repos/:owner/:repo/statuses/:ref" - }, - { - "method": "POST", - "path": "/repos/:owner/:repo/statuses/:ref" - }, - { - "method": "GET", - "path": "/search/repositories" - }, - { - "method": "GET", - "path": "/search/code" - }, - { - "method": "GET", - "path": "/search/issues" - }, - { - "method": "GET", - "path": "/search/users" - }, - { - "method": "GET", - "path": "/legacy/issues/search/:owner/:repository/:state/:keyword" - }, - { - "method": "GET", - "path": "/legacy/repos/search/:keyword" - }, - { - "method": "GET", - "path": "/legacy/user/search/:keyword" - }, - { - "method": "GET", - "path": "/legacy/user/email/:email" - }, - { - "method": "GET", - "path": "/users/:user" - }, - { - "method": "GET", - "path": "/user" - }, - { - "method": "PATCH", - "path": "/user" - }, - { - "method": "GET", - "path": "/users" - }, - { - "method": "GET", - "path": "/user/emails" - }, - { - "method": "POST", - "path": "/user/emails" - }, - { - "method": "DELETE", - "path": "/user/emails" - }, - { - "method": "GET", - "path": "/users/:user/followers" - }, - { - "method": "GET", - "path": "/user/followers" - }, - { - "method": "GET", - "path": "/users/:user/following" - }, - { - "method": "GET", - "path": "/user/following" - }, - { - "method": "GET", - "path": "/user/following/:user" - }, - { - "method": "GET", - "path": "/users/:user/following/:target_user" - }, - { - "method": "PUT", - "path": "/user/following/:user" - }, - { - "method": "DELETE", - "path": "/user/following/:user" - }, - { - "method": "GET", - "path": "/users/:user/keys" - }, - { - "method": "GET", - "path": "/user/keys" - }, - { - "method": "GET", - "path": "/user/keys/:id" - }, - { - "method": "POST", - "path": "/user/keys" - }, - { - "method": "PATCH", - "path": "/user/keys/:id" - }, - { - "method": "DELETE", - "path": "/user/keys/:id" - } - ] -} \ No newline at end of file diff --git a/.github/config.yml b/.github/config.yml deleted file mode 100644 index 5a0fa98b..00000000 --- a/.github/config.yml +++ /dev/null @@ -1,17 +0,0 @@ -# Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome -# Comment to be posted to on first time issues -newIssueWelcomeComment: > - Thanks for opening your first issue here! ๐ŸŽ‰ Be sure to follow the issue template! - If you need help or want to chat with us, join us on Discord https://gofiber.io/discord - -# Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome -# Comment to be posted to on PRs from first time contributors in your repository -newPRWelcomeComment: > - Thanks for opening this pull request! ๐ŸŽ‰ Please check out our contributing guidelines. - If you need help or want to chat with us, join us on Discord https://gofiber.io/discord - -# Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge -# Comment to be posted to on pull requests merged by a first time user -firstPRMergeComment: > - Congrats on merging your first pull request! ๐ŸŽ‰ We here at Fiber are proud of you! - If you need help or want to chat with us, join us on Discord https://gofiber.io/discord diff --git a/.github/index.html b/.github/index.html deleted file mode 100644 index 4115b434..00000000 --- a/.github/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - Test file - - - - Hello, World! - - - \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index 6dcf9497..00000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,11 +0,0 @@ -**Please provide enough information so that others can review your pull request:** - - - -**Explain the *details* for making this change. What existing problem does the pull request solve?** - - - -**Commit formatting** - -Use emojis on commit messages so it provides an easy way of identifying the purpose or intention of a commit. Check out the emoji cheatsheet here: https://gitmoji.carloscuesta.me/ \ No newline at end of file diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index 6b867bd9..00000000 --- a/.github/stale.yml +++ /dev/null @@ -1,26 +0,0 @@ -# Number of days of inactivity before an issue becomes stale -daysUntilStale: 60 - -# Number of days of inactivity before a stale issue is closed -daysUntilClose: 7 - -# Issues with these labels will never be considered stale -exemptLabels: - - pinned - - security - -# Label to use when marking an issue as stale -staleLabel: wontfix - -# Comment to post when marking an issue as stale. Set to `false` to disable -markComment: > - ๐Ÿ‘‹ Hello. Is this still relevant? If so, what is blocking it? Is there - anything you can do to help move it forward? - - > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. - - Thank you for your contributions. - -# Comment to post when closing a stale Issue or Pull Request. Set to `false` to disable -closeComment: > - โšก๏ธ This issue has been automatically closed because it has not had recent activity. \ No newline at end of file diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml deleted file mode 100644 index 1d4036db..00000000 --- a/.github/workflows/benchmark.yml +++ /dev/null @@ -1,28 +0,0 @@ -on: [push] -name: Benchmark -jobs: - Compare: - runs-on: ubuntu-latest - steps: - - name: Install Go - uses: actions/setup-go@v1 - with: - go-version: 1.14.x - - name: Fetch Repository - uses: actions/checkout@v2 - - name: Run Benchmark - run: go test ./... -benchmem -run=^$ -bench . | tee output.txt - - name: Get Previous Benchmark Results - uses: actions/cache@v1 - with: - path: ./cache - key: ${{ runner.os }}-benchmark - - name: Save Benchmark Results - uses: rhysd/github-action-benchmark@v1 - with: - tool: 'go' - output-file-path: output.txt - github-token: ${{ secrets.BENCHMARK_TOKEN }} - fail-on-alert: true - comment-on-alert: true - auto-push: true \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index f92c6776..00000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: "CodeQL" - -on: - push: - branches: [master, ] - pull_request: - # The branches below must be a subset of the branches above - branches: [master] - schedule: - - cron: '0 3 * * 6' - -jobs: - analyse: - name: Analyse - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - with: - # We must fetch at least the immediate parents so that if this is - # a pull request then we can checkout the head. - fetch-depth: 2 - - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - # Override language selection by uncommenting this and choosing your languages - with: - languages: go - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # โ„น๏ธ Command-line programs to run using the OS shell. - # ๐Ÿ“š https://git.io/JvXDl - - # โœ๏ธ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml deleted file mode 100644 index f9bca034..00000000 --- a/.github/workflows/linter.yml +++ /dev/null @@ -1,12 +0,0 @@ -on: [push, pull_request] -name: Linter -jobs: - Golint: - runs-on: ubuntu-latest - steps: - - name: Fetch Repository - uses: actions/checkout@v2 - - name: Run Golint - uses: reviewdog/action-golangci-lint@v1 - with: - golangci_lint_flags: "--tests=false" diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml deleted file mode 100644 index 2d5ad70d..00000000 --- a/.github/workflows/security.yml +++ /dev/null @@ -1,12 +0,0 @@ -on: [push, pull_request] -name: Security -jobs: - Gosec: - runs-on: ubuntu-latest - steps: - - name: Fetch Repository - uses: actions/checkout@v2 - - name: Run Gosec - uses: securego/gosec@master - with: - args: ./... \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 9ab42014..00000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,18 +0,0 @@ -on: [push, pull_request] -name: Test -jobs: - Build: - strategy: - matrix: - go-version: [1.11.x, 1.14.x] - platform: [ubuntu-latest, windows-latest] - runs-on: ${{ matrix.platform }} - steps: - - name: Install Go - uses: actions/setup-go@v1 - with: - go-version: ${{ matrix.go-version }} - - name: Fetch Repository - uses: actions/checkout@v2 - - name: Run Test - run: go test ./... -v -race \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0d589a18..119b1111 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ # Test binary, built with `go test -c` *.test +*.tmp # Output of the go coverage tool, specifically when used with LiteIDE *.out @@ -23,4 +24,7 @@ *.workspace # Dependencies -vendor \ No newline at end of file +/vendor/ +vendor/ +vendor +/Godeps/ \ No newline at end of file diff --git a/LICENSE b/LICENSE index 51201f1f..2b6b59aa 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019-2020 Fenny and Contributors +Copyright (c) 2019-present Joey Burggraaf (@Fenny) and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/app.go b/app.go index 07765a4c..d33541da 100644 --- a/app.go +++ b/app.go @@ -11,9 +11,7 @@ package fiber import ( "bufio" - "crypto/tls" "fmt" - "io" "net" "net/http" "net/http/httputil" @@ -25,20 +23,24 @@ import ( "sync" "time" - utils "github.com/gofiber/utils" - colorable "github.com/mattn/go-colorable" - isatty "github.com/mattn/go-isatty" - fasthttp "github.com/valyala/fasthttp" + "github.com/gofiber/fiber/v2/utils" + "github.com/gofiber/fiber/v2/utils/colorable" + "github.com/gofiber/fiber/v2/utils/isatty" + "github.com/valyala/fasthttp" ) // Version of current package -const Version = "1.14.6" +const Version = "2.0.0" // Map is a shortcut for map[string]interface{}, useful for JSON returns type Map map[string]interface{} // Handler defines a function to serve HTTP requests. -type Handler = func(*Ctx) +type Handler = func(*Ctx) error + +// ErrorHandler defines a function that will process all errors +// returned from any handlers in the stack +type ErrorHandler = func(*Ctx, error) error // Error represents an error that occurred while handling a request. type Error struct { @@ -48,7 +50,6 @@ type Error struct { // App denotes the Fiber application. type App struct { - out io.Writer mutex sync.Mutex // Route stack divided by HTTP methods stack [][]*Route @@ -62,23 +63,15 @@ type App struct { pool sync.Pool // Fasthttp server server *fasthttp.Server - // App settings - Settings *Settings `json:"settings"` + // App config + config Config } -// Settings is a struct holding the server settings. -type Settings struct { - // ErrorHandler is executed when you pass an error in the Next(err) method. - // This function is also executed when middleware.Recover() catches a panic - // Default: func(ctx *Ctx, err error) { - // code := StatusInternalServerError - // if e, ok := err.(*Error); ok { - // code = e.Code - // } - // ctx.Set(HeaderContentType, MIMETextPlainCharsetUTF8) - // ctx.Status(code).SendString(err.Error()) - // } - ErrorHandler func(*Ctx, error) `json:"-"` +// Config is a struct holding the server settings. +type Config struct { + // When set to true, this will spawn multiple Go processes listening on the same port. + // Default: false + Prefork bool `json:"prefork"` // Enables the "Server: value" HTTP header. // Default: "" @@ -111,10 +104,6 @@ type Settings struct { // Default: false ETag bool `json:"etag"` - // When set to true, this will spawn multiple Go processes listening on the same port. - // Default: false - Prefork bool `json:"prefork"` - // Max body size that the server accepts. // Default: 4 * 1024 * 1024 BodyLimit int `json:"body_limit"` @@ -123,28 +112,6 @@ type Settings struct { // Default: 256 * 1024 Concurrency int `json:"concurrency"` - // When set to true, disables keep-alive connections. - // The server will close incoming connections after sending the first response to client. - // Default: false - DisableKeepalive bool `json:"disable_keep_alive"` - - // When set to true, causes the default date header to be excluded from the response. - // Default: false - DisableDefaultDate bool `json:"disable_default_date"` - - // When set to true, causes the default Content-Type header to be excluded from the response. - // Default: false - DisableDefaultContentType bool `json:"disable_default_content_type"` - - // When set to true, disables header normalization. - // By default all header names are normalized: conteNT-tYPE -> Content-Type. - // Default: false - DisableHeaderNormalizing bool `json:"disable_header_normalizing"` - - // When set to true, it will not print out the ยซFiberยป ASCII art and listening address. - // Default: false - DisableStartupMessage bool `json:"disable_startup_message"` - // Views is the interface that wraps the Render function. // Default: nil Views Views `json:"-"` @@ -181,7 +148,56 @@ type Settings struct { // Default: ".fiber.gz" CompressedFileSuffix string `json:"compressed_file_suffix"` - // FEATURE: v1.13 + // ProxyHeader will enable c.IP() to return the value of the given header key + // By default c.IP() will return the Remote IP from the TCP connection + // This property can be useful if you are behind a load balancer: X-Forwarded-* + // NOTE: headers are easily spoofed and the detected IP addresses are unreliable. + // Default: "" + ProxyHeader string `json:"proxy_header"` + + // GETOnly rejects all non-GET requests if set to true. + // This option is useful as anti-DoS protection for servers + // accepting only GET requests. The request size is limited + // by ReadBufferSize if GETOnly is set. + // Server accepts all the requests by default. + GETOnly bool `json:"get_only"` + + // ErrorHandler is executed when an error is returned from fiber.Handler. + // cfg := fiber.Config{} + // cfg.ErrorHandler = func(c *Ctx, err error) error { + // code := StatusInternalServerError + // if e, ok := err.(*Error); ok { + // code = e.Code + // } + // c.Set(HeaderContentType, MIMETextPlainCharsetUTF8) + // return c.Status(code).SendString(err.Error()) + // } + // app := fiber.New(cfg) + ErrorHandler ErrorHandler `json:"-"` + + // When set to true, disables keep-alive connections. + // The server will close incoming connections after sending the first response to client. + // Default: false + DisableKeepalive bool `json:"disable_keep_alive"` + + // When set to true, causes the default date header to be excluded from the response. + // Default: false + DisableDefaultDate bool `json:"disable_default_date"` + + // When set to true, causes the default Content-Type header to be excluded from the response. + // Default: false + DisableDefaultContentType bool `json:"disable_default_content_type"` + + // When set to true, disables header normalization. + // By default all header names are normalized: conteNT-tYPE -> Content-Type. + // Default: false + DisableHeaderNormalizing bool `json:"disable_header_normalizing"` + + // When set to true, it will not print out the ยซFiberยป ASCII art and listening address. + // Default: false + DisableStartupMessage bool `json:"disable_startup_message"` + + // FEATURE: v1.16.x // The router executes the same handler by default if StrictRouting or CaseSensitive is disabled. // Enabling RedirectFixedPath will change this behaviour into a client redirect to the original route path. // Using the status code 301 for GET requests and 308 for all other request methods. @@ -193,47 +209,47 @@ type Static struct { // When set to true, the server tries minimizing CPU usage by caching compressed files. // This works differently than the github.com/gofiber/compression middleware. // Optional. Default value false - Compress bool + Compress bool `json:"compress"` // When set to true, enables byte range requests. // Optional. Default value false - ByteRange bool + ByteRange bool `json:"byte_range"` // When set to true, enables directory browsing. // Optional. Default value false. - Browse bool + Browse bool `json:"browse"` // The name of the index file for serving a directory. // Optional. Default value "index.html". - Index string + Index string `json:"index"` } -// default settings +// Default settings const ( - defaultBodyLimit = 4 * 1024 * 1024 - defaultConcurrency = 256 * 1024 - defaultReadBufferSize = 4096 - defaultWriteBufferSize = 4096 - defaultCompressedFileSuffix = ".fiber.gz" + DefaultBodyLimit = 4 * 1024 * 1024 + DefaultConcurrency = 256 * 1024 + DefaultReadBufferSize = 4096 + DefaultWriteBufferSize = 4096 + DefaultCompressedFileSuffix = ".fiber.gz" ) -var defaultErrorHandler = func(ctx *Ctx, err error) { +var DefaultErrorHandler = func(c *Ctx, err error) error { code := StatusInternalServerError if e, ok := err.(*Error); ok { code = e.Code } - ctx.Set(HeaderContentType, MIMETextPlainCharsetUTF8) - ctx.Status(code).SendString(err.Error()) + c.Set(HeaderContentType, MIMETextPlainCharsetUTF8) + return c.Status(code).SendString(err.Error()) } // New creates a new Fiber named instance. -// myApp := app.New() -// You can pass an optional settings by passing a *Settings struct: -// myApp := app.New(&fiber.Settings{ +// app := fiber.New() +// You can pass an optional settings by passing a Config struct: +// app := fiber.New(fiber.Config{ // Prefork: true, // ServerHeader: "Fiber", // }) -func New(settings ...*Settings) *App { +func New(config ...Config) *App { // Create a new app app := &App{ // Create router stack @@ -245,48 +261,55 @@ func New(settings ...*Settings) *App { return new(Ctx) }, }, - // Set settings - Settings: &Settings{}, + // Create config + config: Config{}, } - - // Overwrite settings if provided - if len(settings) > 0 { - app.Settings = settings[0] + // Override config if provided + if len(config) > 0 { + app.config = config[0] } - - if app.Settings.BodyLimit <= 0 { - app.Settings.BodyLimit = defaultBodyLimit + // Override default values + if app.config.BodyLimit <= 0 { + app.config.BodyLimit = DefaultBodyLimit } - if app.Settings.Concurrency <= 0 { - app.Settings.Concurrency = defaultConcurrency + if app.config.Concurrency <= 0 { + app.config.Concurrency = DefaultConcurrency } - if app.Settings.ReadBufferSize <= 0 { - app.Settings.ReadBufferSize = defaultReadBufferSize + if app.config.ReadBufferSize <= 0 { + app.config.ReadBufferSize = DefaultReadBufferSize } - if app.Settings.WriteBufferSize <= 0 { - app.Settings.WriteBufferSize = defaultWriteBufferSize + if app.config.WriteBufferSize <= 0 { + app.config.WriteBufferSize = DefaultWriteBufferSize } - if app.Settings.CompressedFileSuffix == "" { - app.Settings.CompressedFileSuffix = defaultCompressedFileSuffix + if app.config.CompressedFileSuffix == "" { + app.config.CompressedFileSuffix = DefaultCompressedFileSuffix } - if app.Settings.ErrorHandler == nil { - app.Settings.ErrorHandler = defaultErrorHandler - } - if app.Settings.Immutable { + if app.config.Immutable { getBytes, getString = getBytesImmutable, getStringImmutable } - + if app.config.ErrorHandler == nil { + app.config.ErrorHandler = DefaultErrorHandler + } + // Init app + app.init() // Return app return app } -// Use registers a middleware route. -// Middleware matches requests beginning with the provided prefix. -// Providing a prefix is optional, it defaults to "/". +// Use registers a middleware route. that will match requests +// that contain the provided prefix ( which is optional and defaults to "/" ). // -// app.Use(handler) -// app.Use("/api", handler) -// app.Use("/api", handler, handler) +// app.Use(func(c *fiber.Ctx) error { +// return c.Next() +// }) +// app.Use("/api", func(c *fiber.Ctx) error { +// return c.Next() +// }) +// app.Use("/api", handler(), func(c *fiber.Ctx) error { +// return c.Next() +// }) +// +// This method will match all HTTP verbs: GET, POST, PUT, HEAD etc... func (app *App) Use(args ...interface{}) Router { var prefix string var handlers []Handler @@ -297,6 +320,15 @@ func (app *App) Use(args ...interface{}) Router { prefix = arg case Handler: handlers = append(handlers, arg) + case *App: + stack := arg.Stack() + for m := range stack { + for r := range stack[m] { + route := app.copyRoute(stack[m][r]) + app.addRoute(route.Method, app.addPrefixToRoute(prefix, route)) + } + } + return app default: panic(fmt.Sprintf("use: invalid handler %v\n", reflect.TypeOf(arg))) } @@ -308,12 +340,7 @@ func (app *App) Use(args ...interface{}) Router { // Get registers a route for GET methods that requests a representation // of the specified resource. Requests using GET should only retrieve data. func (app *App) Get(path string, handlers ...Handler) Router { - route := app.register(MethodGet, path, handlers...) - // Add HEAD route - headRoute := route - app.addRoute(MethodHead, &headRoute) - - return app + return app.Add(MethodHead, path, handlers...).Add(MethodGet, path, handlers...) } // Head registers a route for HEAD methods that asks for a response identical @@ -363,27 +390,27 @@ func (app *App) Patch(path string, handlers ...Handler) Router { return app.Add(MethodPatch, path, handlers...) } -// Add ... +// Add allows you to specify a HTTP method to register a route func (app *App) Add(method, path string, handlers ...Handler) Router { - app.register(method, path, handlers...) - return app + return app.register(method, path, handlers...) } -// Static ... +// Static will create a file server serving static files func (app *App) Static(prefix, root string, config ...Static) Router { - app.registerStatic(prefix, root, config...) - return app + return app.registerStatic(prefix, root, config...) } -// All ... +// All will register the handler on all HTTP methods func (app *App) All(path string, handlers ...Handler) Router { for _, method := range intMethod { - app.Add(method, path, handlers...) + _ = app.Add(method, path, handlers...) } return app } // Group is used for Routes with common prefix to define a new sub-router with optional middleware. +// api := app.Group("/api") +// api.Get("/users", handler()) func (app *App) Group(prefix string, handlers ...Handler) Router { if len(handlers) > 0 { app.register(methodUse, prefix, handlers...) @@ -391,126 +418,81 @@ func (app *App) Group(prefix string, handlers ...Handler) Router { return &Group{prefix: prefix, app: app} } -// Error makes it compatible with `error` interface. +// Error makes it compatible with the `error` interface. func (e *Error) Error() string { return e.Message } -// NewError creates a new HTTPError instance. +// NewError creates a new HTTPError instance with an optional message func NewError(code int, message ...string) *Error { - e := &Error{code, utils.StatusMessage(code)} + e := &Error{ + Code: code, + } if len(message) > 0 { e.Message = message[0] + } else { + e.Message = utils.StatusMessage(code) } return e } -// Routes returns all registered routes -// for _, r := range app.Routes() { -// fmt.Printf("%s\t%s\n", r.Method, r.Path) -// } -func (app *App) Routes() []*Route { - fmt.Println("routes is deprecated since v1.13.2, please use `app.Stack()` to access the raw router stack") - routes := make([]*Route, 0) - for m := range app.stack { - stackLoop: - for r := range app.stack[m] { - // Don't duplicate USE routesCount - if app.stack[m][r].use { - for i := range routes { - if routes[i].use && routes[i].Path == app.stack[m][r].Path { - continue stackLoop - } - } - } - routes = append(routes, app.stack[m][r]) - } - } - return routes -} - // Listener can be used to pass a custom listener. -// You can pass an optional *tls.Config to enable TLS. -// This method does not support the Prefork feature -// To use Prefork, please use app.Listen() -func (app *App) Listener(ln net.Listener, tlsconfig ...*tls.Config) error { - // Update server settings - app.init() - // TLS config - if len(tlsconfig) > 0 { - ln = tls.NewListener(ln, tlsconfig[0]) +func (app *App) Listener(ln net.Listener) error { + // Prefork is supported for custom listeners + if app.config.Prefork { + addr, tls := lnMetadata(ln) + return app.prefork(addr, tls) } + // Print startup message - if !app.Settings.DisableStartupMessage { - app.startupMessage(ln.Addr().String(), len(tlsconfig) > 0, "") + if !app.config.DisableStartupMessage { + app.startupMessage(ln.Addr().String(), false, "") } + + // TODO: Detect TLS return app.server.Serve(ln) } -// Listen serves HTTP requests from the given addr or port. -// You can pass an optional *tls.Config to enable TLS. +// Listen serves HTTP requests from the given addr. // -// app.Listen(8080) -// app.Listen("8080") // app.Listen(":8080") // app.Listen("127.0.0.1:8080") -func (app *App) Listen(address interface{}, tlsconfig ...*tls.Config) error { - // Convert address to string - addr, ok := address.(string) - if !ok { - port, ok := address.(int) - if !ok { - return fmt.Errorf("listen: host must be an `int` port or `string` address") - } - addr = strconv.Itoa(port) - } - if !strings.Contains(addr, ":") { - addr = ":" + addr - } - // Update server settings - app.init() +func (app *App) Listen(addr string) error { // Start prefork - if app.Settings.Prefork { - return app.prefork(addr, tlsconfig...) - } - // Set correct network protocol - network := "tcp4" - if isIPv6(addr) { - network = "tcp6" + if app.config.Prefork { + return app.prefork(addr, nil) } // Setup listener - ln, err := net.Listen(network, addr) + ln, err := net.Listen("tcp4", addr) if err != nil { return err } - // Add TLS config if provided - if len(tlsconfig) > 0 { - ln = tls.NewListener(ln, tlsconfig[0]) - } // Print startup message - if !app.Settings.DisableStartupMessage { - app.startupMessage(ln.Addr().String(), len(tlsconfig) > 0, "") + if !app.config.DisableStartupMessage { + app.startupMessage(ln.Addr().String(), false, "") } // Start listening return app.server.Serve(ln) } -// Handler returns the server handler. -func (app *App) Handler() fasthttp.RequestHandler { - app.init() - return app.handler +// Config returns the app config as value ( read-only ). +func (app *App) Config() Config { + return app.config } // Handler returns the server handler. +func (app *App) Handler() fasthttp.RequestHandler { + return app.handler +} + +// Stack returns the raw router stack. func (app *App) Stack() [][]*Route { return app.stack } -// Shutdown gracefully -// shuts down the server without interrupting any active connections. +// Shutdown gracefully shuts down the server without interrupting any active connections. // Shutdown works by first closing all open listeners and then waiting indefinitely for all connections to return to idle and then shut down. // -// When Shutdown is called, Serve, ListenAndServe, and ListenAndServeTLS immediately return nil. // Make sure the program doesn't exit and waits instead for Shutdown to return. // // Shutdown does not close keepalive connections so its recommended to set ReadTimeout to something else than 0. @@ -525,33 +507,38 @@ func (app *App) Shutdown() error { // Test is used for internal debugging by passing a *http.Request. // Timeout is optional and defaults to 1s, -1 will disable it completely. -func (app *App) Test(request *http.Request, msTimeout ...int) (*http.Response, error) { - timeout := 1000 // 1 second default +func (app *App) Test(req *http.Request, msTimeout ...int) (resp *http.Response, err error) { + // Set timeout + timeout := 1000 if len(msTimeout) > 0 { timeout = msTimeout[0] } + // Add Content-Length if not provided with body - if request.Body != http.NoBody && request.Header.Get("Content-Length") == "" { - request.Header.Add("Content-Length", strconv.FormatInt(request.ContentLength, 10)) + if req.Body != http.NoBody && req.Header.Get(HeaderContentLength) == "" { + req.Header.Add(HeaderContentLength, strconv.FormatInt(req.ContentLength, 10)) } + // Dump raw http request - dump, err := httputil.DumpRequest(request, true) + dump, err := httputil.DumpRequest(req, true) if err != nil { return nil, err } - // Update server settings - app.init() + // Create test connection conn := new(testConn) + // Write raw http request if _, err = conn.r.Write(dump); err != nil { return nil, err } + // Serve conn to server channel := make(chan error) go func() { channel <- app.server.ServeConn(conn) }() + // Wait for callback if timeout >= 0 { // With timeout @@ -564,19 +551,17 @@ func (app *App) Test(request *http.Request, msTimeout ...int) (*http.Response, e // Without timeout err = <-channel } + // Check for errors - if err != nil { + if err != nil && err != fasthttp.ErrGetOnly { return nil, err } + // Read response buffer := bufio.NewReader(&conn.w) + // Convert raw http response to *http.Response - resp, err := http.ReadResponse(buffer, request) - if err != nil { - return nil, err - } - // Return *http.Response - return resp, nil + return http.ReadResponse(buffer, req) } type disableLogger struct{} @@ -586,128 +571,148 @@ func (dl *disableLogger) Printf(format string, args ...interface{}) { } func (app *App) init() *App { - // Lock application + // lock application app.mutex.Lock() - defer app.mutex.Unlock() - // Load view engine if provided - if app.Settings != nil { - // Only load templates if an view engine is specified - if app.Settings.Views != nil { - if err := app.Settings.Views.Load(); err != nil { - fmt.Printf("views: %v\n", err) + // Only load templates if an view engine is specified + if app.config.Views != nil { + if err := app.config.Views.Load(); err != nil { + fmt.Printf("views: %v\n", err) + } + } + + // create fasthttp server + app.server = &fasthttp.Server{ + Logger: &disableLogger{}, + LogAllErrors: false, + ErrorHandler: func(fctx *fasthttp.RequestCtx, err error) { + c := app.AcquireCtx(fctx) + if _, ok := err.(*fasthttp.ErrSmallBuffer); ok { + err = ErrRequestHeaderFieldsTooLarge + } else if netErr, ok := err.(*net.OpError); ok && netErr.Timeout() { + err = ErrRequestTimeout + } else if err == fasthttp.ErrBodyTooLarge { + err = ErrRequestEntityTooLarge + } else if err == fasthttp.ErrGetOnly { + err = ErrMethodNotAllowed + } else if strings.Contains(err.Error(), "timeout") { + err = ErrRequestTimeout + } else { + err = ErrBadRequest } - } + app.config.ErrorHandler(c, err) + app.ReleaseCtx(c) + }, } - if app.server == nil { - app.server = &fasthttp.Server{ - Logger: &disableLogger{}, - LogAllErrors: false, - ErrorHandler: func(fctx *fasthttp.RequestCtx, err error) { - ctx := app.AcquireCtx(fctx) - if _, ok := err.(*fasthttp.ErrSmallBuffer); ok { - ctx.err = ErrRequestHeaderFieldsTooLarge - } else if netErr, ok := err.(*net.OpError); ok && netErr.Timeout() { - ctx.err = ErrRequestTimeout - } else if len(err.Error()) == 33 && err.Error() == "body size exceeds the given limit" { - ctx.err = ErrRequestEntityTooLarge - } else { - ctx.err = ErrBadRequest - } - app.Settings.ErrorHandler(ctx, ctx.err) - app.ReleaseCtx(ctx) - }, - } - } - if app.server.Handler == nil { - app.server.Handler = app.handler - } - app.server.Name = app.Settings.ServerHeader - app.server.Concurrency = app.Settings.Concurrency - app.server.NoDefaultDate = app.Settings.DisableDefaultDate - app.server.NoDefaultContentType = app.Settings.DisableDefaultContentType - app.server.DisableHeaderNamesNormalizing = app.Settings.DisableHeaderNormalizing - app.server.DisableKeepalive = app.Settings.DisableKeepalive - app.server.MaxRequestBodySize = app.Settings.BodyLimit - app.server.NoDefaultServerHeader = app.Settings.ServerHeader == "" - app.server.ReadTimeout = app.Settings.ReadTimeout - app.server.WriteTimeout = app.Settings.WriteTimeout - app.server.IdleTimeout = app.Settings.IdleTimeout - app.server.ReadBufferSize = app.Settings.ReadBufferSize - app.server.WriteBufferSize = app.Settings.WriteBufferSize - app.buildTree() + + // fasthttp server settings + app.server.Handler = app.handler + app.server.Name = app.config.ServerHeader + app.server.Concurrency = app.config.Concurrency + app.server.NoDefaultDate = app.config.DisableDefaultDate + app.server.NoDefaultContentType = app.config.DisableDefaultContentType + app.server.DisableHeaderNamesNormalizing = app.config.DisableHeaderNormalizing + app.server.DisableKeepalive = app.config.DisableKeepalive + app.server.MaxRequestBodySize = app.config.BodyLimit + app.server.NoDefaultServerHeader = app.config.ServerHeader == "" + app.server.ReadTimeout = app.config.ReadTimeout + app.server.WriteTimeout = app.config.WriteTimeout + app.server.IdleTimeout = app.config.IdleTimeout + app.server.ReadBufferSize = app.config.ReadBufferSize + app.server.WriteBufferSize = app.config.WriteBufferSize + app.server.GetOnly = app.config.GETOnly + + // unlock application + app.mutex.Unlock() return app } -const ( - cBlack = "\u001b[90m" - cRed = "\u001b[91m" - // cGreen = "\u001b[92m" - // cYellow = "\u001b[93m" - // cBlue = "\u001b[94m" - // cMagenta = "\u001b[95m" - cCyan = "\u001b[96m" - // cWhite = "\u001b[97m" - cReset = "\u001b[0m" -) - func (app *App) startupMessage(addr string, tls bool, pids string) { // ignore child processes - if app.IsChild() { + if IsChild() { return } + // ascii logo var logo string - logo += `%s _______ __ %s` + "\n" - logo += `%s ____%s / ____(_) /_ ___ _____ %s` + "\n" - logo += `%s_____%s / /_ / / __ \/ _ \/ ___/ %s` + "\n" - logo += `%s __%s / __/ / / /_/ / __/ / %s` + "\n" - logo += `%s /_/ /_/_.___/\___/_/%s %s` + "" - logo += cRed + "v2 will be released on 15 September 2020!\nPlease visit https://gofiber.io/v2 for more information.\n" + cReset - host, port := parseAddr(addr) - padding := strconv.Itoa(len(host)) - if len(host) <= 4 { - padding = "5" - } - var ( - tlsStr = "FALSE" - preforkStr = "FALSE" - handlerCount = strconv.Itoa(app.handlerCount) - osName = utils.ToUpper(runtime.GOOS) - cpuThreads = runtime.NumCPU() - pid = os.Getpid() + // logo += `%s _______ __ %s` + "\n" + // logo += `%s ____%s / ____(_) /_ ___ _____ %s` + "\n" + // logo += `%s_____%s / /_ / / __ \/ _ \/ ___/ %s` + "\n" + // logo += `%s __%s / __/ / / /_/ / __/ / %s` + "\n" + // logo += `%s /_/ /_/_.___/\___/_/%s %s` + "\n" + + logo += "\n%s" + logo += " โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”\n" + logo += " โ”‚ %sFiber v%s%s โ”‚\n" + logo += " โ”‚ Express inspired web framework โ”‚\n" + logo += " โ”‚ โ”‚\n" + logo += " โ”‚ Host : %s %s : OS โ”‚\n" + logo += " โ”‚ Port : %s %s : Threads โ”‚\n" + logo += " โ”‚ TLS : %s %s : Prefork โ”‚\n" + logo += " โ”‚ Handlers : %s %s : PID โ”‚\n" + logo += " โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜" + logo += "%s\n" + + const ( + cBlack = "\u001b[90m" + cRed = "\u001b[91m" + cCyan = "\u001b[96m" + cGreen = "\u001b[92m" + // cYellow = "\u001b[93m" + // cBlue = "\u001b[94m" + // cMagenta = "\u001b[95m" + // cWhite = "\u001b[97m" + cReset = "\u001b[0m" ) + + clrL := func(v interface{}) string { + if v == "disabled" { + return fmt.Sprintf("%s%15v%s", cRed, v, cBlack) + } + if v == "enabled" { + return fmt.Sprintf("%s%15v%s", cGreen, v, cBlack) + } + return fmt.Sprintf("%s%15v%s", cCyan, v, cBlack) + } + clR := func(v interface{}) string { + if v == "disabled" { + return fmt.Sprintf("%s%-15v%s", cRed, v, cBlack) + } + if v == "enabled" { + return fmt.Sprintf("%s%-15v%s", cGreen, v, cBlack) + } + return fmt.Sprintf("%s%-15v%s", cCyan, v, cBlack) + } + + host, port := parseAddr(addr) + var ( + isTLS = "disabled" + isPrefork = "disabled" + ) + if host == "" { host = "0.0.0.0" } if tls { - tlsStr = "TRUE" + isTLS = "enabled" } - if app.Settings.Prefork { - preforkStr = "TRUE" + if app.config.Prefork { + isPrefork = "enabled" } - // tabwriter makes sure the spacing are consistent across different values - // colorable handles the escape sequence for stdout using ascii color codes - host = fmt.Sprintf("%-"+padding+"s", host) - port = fmt.Sprintf("%-"+padding+"s", port) - tlsStr = fmt.Sprintf("%-"+padding+"s", tlsStr) - handlerCount = fmt.Sprintf("%-"+padding+"s", handlerCount) - app.out = colorable.NewColorableStdout() - // Check if colors are supported - if os.Getenv("TERM") == "dumb" || - (!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) { - app.out = colorable.NewNonColorable(os.Stdout) + out := colorable.NewColorableStdout() + if os.Getenv("TERM") == "dumb" || (!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) { + out = colorable.NewNonColorable(os.Stdout) } - // simple Sprintf function that defaults back to black - cyan := func(v interface{}) string { - return fmt.Sprintf("%s%v%s", cCyan, v, cBlack) - } - // Build startup banner - fmt.Fprintf(app.out, logo, cBlack, cBlack, - cCyan, cBlack, fmt.Sprintf(" HOST %s OS %s", cyan(host), cyan(osName)), - cCyan, cBlack, fmt.Sprintf(" PORT %s THREADS %s", cyan(port), cyan(cpuThreads)), - cCyan, cBlack, fmt.Sprintf(" TLS %s PREFORK %s", cyan(tlsStr), cyan(preforkStr)), - cBlack, cyan(Version), fmt.Sprintf(" HANDLERS %s PID %s%s%s\n", cyan(handlerCount), cyan(pid), pids, cReset), + + fmt.Fprintf(out, logo, + cBlack, + cCyan, Version, cBlack, + clR(host), clrL(utils.ToUpper(runtime.GOOS)), + clR(port), clrL(runtime.NumCPU()), + clR(isTLS), clrL(isPrefork), + clR(app.handlerCount), clrL(os.Getpid()), + cReset, ) + } diff --git a/app_test.go b/app_test.go index 11923df4..853c92fe 100644 --- a/app_test.go +++ b/app_test.go @@ -20,11 +20,15 @@ import ( "testing" "time" - utils "github.com/gofiber/utils" - fasthttp "github.com/valyala/fasthttp" - fasthttputil "github.com/valyala/fasthttp/fasthttputil" + "github.com/gofiber/fiber/v2/utils" + "github.com/valyala/fasthttp" + "github.com/valyala/fasthttp/fasthttputil" ) +var testEmptyHandler = func(c *Ctx) error { + return nil +} + func testStatus200(t *testing.T, app *App, url string, method string) { req := httptest.NewRequest(method, url, nil) @@ -36,11 +40,13 @@ func testStatus200(t *testing.T, app *App, url string, method string) { func Test_App_MethodNotAllowed(t *testing.T) { app := New() - app.Use(func(ctx *Ctx) { ctx.Next() }) + app.Use(func(c *Ctx) error { + return c.Next() + }) - app.Post("/", func(c *Ctx) {}) + app.Post("/", testEmptyHandler) - app.Options("/", func(c *Ctx) {}) + app.Options("/", testEmptyHandler) resp, err := app.Test(httptest.NewRequest("POST", "/", nil)) utils.AssertEqual(t, nil, err) @@ -62,7 +68,7 @@ func Test_App_MethodNotAllowed(t *testing.T) { utils.AssertEqual(t, 405, resp.StatusCode) utils.AssertEqual(t, "POST, OPTIONS", resp.Header.Get(HeaderAllow)) - app.Get("/", func(c *Ctx) {}) + app.Get("/", testEmptyHandler) resp, err = app.Test(httptest.NewRequest("TRACE", "/", nil)) utils.AssertEqual(t, nil, err) @@ -83,22 +89,21 @@ func Test_App_MethodNotAllowed(t *testing.T) { func Test_App_Custom_Middleware_404_Should_Not_SetMethodNotAllowed(t *testing.T) { app := New() - app.Use(func(ctx *Ctx) { - ctx.Status(404) + app.Use(func(c *Ctx) error { + return c.SendStatus(404) }) - app.Post("/", func(c *Ctx) {}) + app.Post("/", testEmptyHandler) resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) utils.AssertEqual(t, nil, err) utils.AssertEqual(t, 404, resp.StatusCode) - g := app.Group("/with-next", func(ctx *Ctx) { - ctx.Status(404) - ctx.Next() + g := app.Group("/with-next", func(c *Ctx) error { + return c.Status(404).Next() }) - g.Post("/", func(c *Ctx) {}) + g.Post("/", testEmptyHandler) resp, err = app.Test(httptest.NewRequest("GET", "/with-next", nil)) utils.AssertEqual(t, nil, err) @@ -111,7 +116,7 @@ func Test_App_ServerErrorHandler_SmallReadBuffer(t *testing.T) { ) app := New() - app.Get("/", func(c *Ctx) { + app.Get("/", func(c *Ctx) error { panic(errors.New("should never called")) }) @@ -132,13 +137,13 @@ func Test_App_ServerErrorHandler_SmallReadBuffer(t *testing.T) { ) } -func Test_App_ErrorHandler(t *testing.T) { - app := New(&Settings{ +func Test_App_Errors(t *testing.T) { + app := New(Config{ BodyLimit: 4, }) - app.Get("/", func(c *Ctx) { - c.Next(errors.New("hi, i'm an error")) + app.Get("/", func(c *Ctx) error { + return errors.New("hi, i'm an error") }) resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) @@ -156,14 +161,14 @@ func Test_App_ErrorHandler(t *testing.T) { } func Test_App_ErrorHandler_Custom(t *testing.T) { - app := New(&Settings{ - ErrorHandler: func(ctx *Ctx, err error) { - ctx.Status(200).SendString("hi, i'm an custom error") + app := New(Config{ + ErrorHandler: func(c *Ctx, err error) error { + return c.Status(200).SendString("hi, i'm an custom error") }, }) - app.Get("/", func(c *Ctx) { - c.Next(errors.New("hi, i'm an error")) + app.Get("/", func(c *Ctx) error { + return errors.New("hi, i'm an error") }) resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) @@ -178,17 +183,17 @@ func Test_App_ErrorHandler_Custom(t *testing.T) { func Test_App_Nested_Params(t *testing.T) { app := New() - app.Get("/test", func(c *Ctx) { - c.Status(400).Send("Should move on") + app.Get("/test", func(c *Ctx) error { + return c.Status(400).Send([]byte("Should move on")) }) - app.Get("/test/:param", func(c *Ctx) { - c.Status(400).Send("Should move on") + app.Get("/test/:param", func(c *Ctx) error { + return c.Status(400).Send([]byte("Should move on")) }) - app.Get("/test/:param/test", func(c *Ctx) { - c.Status(400).Send("Should move on") + app.Get("/test/:param/test", func(c *Ctx) error { + return c.Status(400).Send([]byte("Should move on")) }) - app.Get("/test/:param/test/:param2", func(c *Ctx) { - c.Status(200).Send("Good job") + app.Get("/test/:param/test/:param2", func(c *Ctx) error { + return c.Status(200).Send([]byte("Good job")) }) req := httptest.NewRequest("GET", "/test/john/test/doe", nil) @@ -198,20 +203,37 @@ func Test_App_Nested_Params(t *testing.T) { utils.AssertEqual(t, 200, resp.StatusCode, "Status code") } +// func Test_App_Use_App(t *testing.T) { +// micro := New() +// micro.Get("/doe", func(c *Ctx) error { +// return c.SendStatus(StatusOK) +// }) + +// app := New() +// app.Use("/john", micro) + +// resp, err := app.Test(httptest.NewRequest("GET", "/john/doe", nil)) +// utils.AssertEqual(t, nil, err, "app.Test(req)") +// utils.AssertEqual(t, 200, resp.StatusCode, "Status code") +// } + func Test_App_Use_Params(t *testing.T) { app := New() - app.Use("/prefix/:param", func(c *Ctx) { + app.Use("/prefix/:param", func(c *Ctx) error { utils.AssertEqual(t, "john", c.Params("param")) + return nil }) - app.Use("/foo/:bar?", func(c *Ctx) { + app.Use("/foo/:bar?", func(c *Ctx) error { utils.AssertEqual(t, "foobar", c.Params("bar", "foobar")) + return nil }) - app.Use("/:param/*", func(c *Ctx) { + app.Use("/:param/*", func(c *Ctx) error { utils.AssertEqual(t, "john", c.Params("param")) utils.AssertEqual(t, "doe", c.Params("*")) + return nil }) resp, err := app.Test(httptest.NewRequest("GET", "/prefix/john", nil)) @@ -244,59 +266,59 @@ func Test_App_Add_Method_Test(t *testing.T) { utils.AssertEqual(t, "add: invalid http method JOHN\n", fmt.Sprintf("%v", err)) } }() - app.Add("JOHN", "/doe", func(c *Ctx) { - - }) -} - -func Test_App_Listen_TLS(t *testing.T) { - app := New() - - // Create tls certificate - cer, err := tls.LoadX509KeyPair("./.github/TEST_DATA/ssl.pem", "./.github/TEST_DATA/ssl.key") - if err != nil { - utils.AssertEqual(t, nil, err) - } - config := &tls.Config{Certificates: []tls.Certificate{cer}} - - go func() { - time.Sleep(1000 * time.Millisecond) - utils.AssertEqual(t, nil, app.Shutdown()) - }() - - utils.AssertEqual(t, nil, app.Listen(3078, config)) + app.Add("JOHN", "/doe", testEmptyHandler) } func Test_App_Listener_TLS(t *testing.T) { app := New() // Create tls certificate - cer, err := tls.LoadX509KeyPair("./.github/TEST_DATA/ssl.pem", "./.github/TEST_DATA/ssl.key") + cer, err := tls.LoadX509KeyPair("./.github/testdata/ssl.pem", "./.github/testdata/ssl.key") if err != nil { utils.AssertEqual(t, nil, err) } config := &tls.Config{Certificates: []tls.Certificate{cer}} + ln, err := net.Listen("tcp4", ":3078") + utils.AssertEqual(t, nil, err) + + ln = tls.NewListener(ln, config) + go func() { time.Sleep(1000 * time.Millisecond) utils.AssertEqual(t, nil, app.Shutdown()) }() - ln, err := net.Listen("tcp4", ":3055") - utils.AssertEqual(t, nil, err) - - utils.AssertEqual(t, nil, app.Listener(ln, config)) + utils.AssertEqual(t, nil, app.Listener(ln)) } + +// go test -run Test_App_GETOnly +func Test_App_GETOnly(t *testing.T) { + app := New(Config{ + GETOnly: true, + }) + + app.Post("/", func(c *Ctx) error { + return c.SendString("Hello ๐Ÿ‘‹!") + }) + + req := httptest.NewRequest("POST", "/", nil) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusMethodNotAllowed, resp.StatusCode, "Status code") +} + func Test_App_Use_Params_Group(t *testing.T) { app := New() group := app.Group("/prefix/:param/*") - group.Use("/", func(c *Ctx) { - c.Next() + group.Use("/", func(c *Ctx) error { + return c.Next() }) - group.Get("/test", func(c *Ctx) { + group.Get("/test", func(c *Ctx) error { utils.AssertEqual(t, "john", c.Params("param")) utils.AssertEqual(t, "doe", c.Params("*")) + return nil }) resp, err := app.Test(httptest.NewRequest("GET", "/prefix/john/doe/test", nil)) @@ -305,12 +327,12 @@ func Test_App_Use_Params_Group(t *testing.T) { } func Test_App_Chaining(t *testing.T) { - n := func(c *Ctx) { - c.Next() + n := func(c *Ctx) error { + return c.Next() } app := New() - app.Use("/john", n, n, n, n, func(c *Ctx) { - c.Status(202) + app.Use("/john", n, n, n, n, func(c *Ctx) error { + return c.SendStatus(202) }) // check handler count for registered HEAD route utils.AssertEqual(t, 5, len(app.stack[methodInt(MethodHead)][0].Handlers), "app.Test(req)") @@ -321,8 +343,8 @@ func Test_App_Chaining(t *testing.T) { utils.AssertEqual(t, nil, err, "app.Test(req)") utils.AssertEqual(t, 202, resp.StatusCode, "Status code") - app.Get("/test", n, n, n, n, func(c *Ctx) { - c.Status(203) + app.Get("/test", n, n, n, n, func(c *Ctx) error { + return c.SendStatus(203) }) req = httptest.NewRequest("GET", "/test", nil) @@ -336,18 +358,19 @@ func Test_App_Chaining(t *testing.T) { func Test_App_Order(t *testing.T) { app := New() - app.Get("/test", func(c *Ctx) { - c.Write("1") - c.Next() + app.Get("/test", func(c *Ctx) error { + c.Write([]byte("1")) + return c.Next() }) - app.All("/test", func(c *Ctx) { - c.Write("2") - c.Next() + app.All("/test", func(c *Ctx) error { + c.Write([]byte("2")) + return c.Next() }) - app.Use(func(c *Ctx) { - c.Write("3") + app.Use(func(c *Ctx) error { + c.Write([]byte("3")) + return nil }) req := httptest.NewRequest("GET", "/test", nil) @@ -361,7 +384,7 @@ func Test_App_Order(t *testing.T) { utils.AssertEqual(t, "123", string(body)) } func Test_App_Methods(t *testing.T) { - var dummyHandler = func(c *Ctx) {} + var dummyHandler = testEmptyHandler app := New() @@ -402,20 +425,16 @@ func Test_App_Methods(t *testing.T) { func Test_App_New(t *testing.T) { app := New() - app.Get("/", func(*Ctx) { + app.Get("/", testEmptyHandler) - }) - - appConfig := New(&Settings{ + appConfig := New(Config{ Immutable: true, }) - appConfig.Get("/", func(*Ctx) { - - }) + appConfig.Get("/", testEmptyHandler) } func Test_App_Shutdown(t *testing.T) { - app := New(&Settings{ + app := New(Config{ DisableStartupMessage: true, }) if err := app.Shutdown(); err != nil { @@ -483,9 +502,9 @@ func Test_App_Static_Direct(t *testing.T) { func Test_App_Static_Group(t *testing.T) { app := New() - grp := app.Group("/v1", func(c *Ctx) { + grp := app.Group("/v1", func(c *Ctx) error { c.Set("Test-Header", "123") - c.Next() + return c.Next() }) grp.Static("/v2", "./.github/FUNDING.yml") @@ -588,15 +607,15 @@ func Test_App_Mixed_Routes_WithSameLen(t *testing.T) { app := New() // middleware - app.Use(func(ctx *Ctx) { - ctx.Set("TestHeader", "TestValue") - ctx.Next() + app.Use(func(c *Ctx) error { + c.Set("TestHeader", "TestValue") + return c.Next() }) // routes with the same length app.Static("/tesbar", "./.github") - app.Get("/foobar", func(ctx *Ctx) { - ctx.Send("FOO_BAR") - ctx.Type("html") + app.Get("/foobar", func(c *Ctx) error { + c.Type("html") + return c.Send([]byte("FOO_BAR")) }) // match get route @@ -637,7 +656,7 @@ func Test_App_Group_Invalid(t *testing.T) { } func Test_App_Group(t *testing.T) { - var dummyHandler = func(c *Ctx) {} + var dummyHandler = testEmptyHandler app := New() @@ -695,18 +714,18 @@ func Test_App_Group(t *testing.T) { func Test_App_Deep_Group(t *testing.T) { runThroughCount := 0 - var dummyHandler = func(c *Ctx) { + var dummyHandler = func(c *Ctx) error { runThroughCount++ - c.Next() + return c.Next() } app := New() gAPI := app.Group("/api", dummyHandler) gV1 := gAPI.Group("/v1", dummyHandler) gUser := gV1.Group("/user", dummyHandler) - gUser.Get("/authenticate", func(ctx *Ctx) { + gUser.Get("/authenticate", func(c *Ctx) error { runThroughCount++ - ctx.SendStatus(200) + return c.SendStatus(200) }) testStatus200(t, app, "/api/v1/user/authenticate", "GET") utils.AssertEqual(t, 4, runThroughCount, "Loop count") @@ -715,12 +734,13 @@ func Test_App_Deep_Group(t *testing.T) { // go test -run Test_App_Next_Method func Test_App_Next_Method(t *testing.T) { app := New() - app.Settings.DisableStartupMessage = true + app.config.DisableStartupMessage = true - app.Use(func(c *Ctx) { + app.Use(func(c *Ctx) error { utils.AssertEqual(t, "GET", c.Method()) c.Next() utils.AssertEqual(t, "GET", c.Method()) + return nil }) resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) @@ -732,30 +752,24 @@ func Test_App_Next_Method(t *testing.T) { func Test_App_Listen(t *testing.T) { app := New() - utils.AssertEqual(t, false, app.Listen(1.23) == nil) + go func() { + time.Sleep(1000 * time.Millisecond) + utils.AssertEqual(t, nil, app.Shutdown()) + }() - utils.AssertEqual(t, false, app.Listen(":1.23") == nil) + utils.AssertEqual(t, nil, app.Listen(":4003")) go func() { time.Sleep(1000 * time.Millisecond) utils.AssertEqual(t, nil, app.Shutdown()) }() - utils.AssertEqual(t, nil, app.Listen(4003)) - - go func() { - time.Sleep(1000 * time.Millisecond) - utils.AssertEqual(t, nil, app.Shutdown()) - }() - - utils.AssertEqual(t, nil, app.Listen("[::]:4010")) + utils.AssertEqual(t, nil, app.Listen(":4010")) } // go test -run Test_App_Listener func Test_App_Listener(t *testing.T) { - app := New(&Settings{ - Prefork: true, - }) + app := New() go func() { time.Sleep(500 * time.Millisecond) @@ -766,16 +780,25 @@ func Test_App_Listener(t *testing.T) { utils.AssertEqual(t, nil, app.Listener(ln)) } +// go test -v -run=^$ -bench=Benchmark_AcquireCtx -benchmem -count=4 +func Benchmark_AcquireCtx(b *testing.B) { + app := New() + for n := 0; n < b.N; n++ { + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + app.ReleaseCtx(c) + } +} + // go test -v -run=^$ -bench=Benchmark_App_ETag -benchmem -count=4 func Benchmark_App_ETag(b *testing.B) { app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) - c.Send("Hello, World!") + c.Send([]byte("Hello, World!")) for n := 0; n < b.N; n++ { setETag(c, false) } - utils.AssertEqual(b, `"13-1831710635"`, string(c.Fasthttp.Response.Header.Peek(HeaderETag))) + utils.AssertEqual(b, `"13-1831710635"`, string(c.Response().Header.Peek(HeaderETag))) } // go test -v -run=^$ -bench=Benchmark_App_ETag_Weak -benchmem -count=4 @@ -783,11 +806,11 @@ func Benchmark_App_ETag_Weak(b *testing.B) { app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) - c.Send("Hello, World!") + c.Send([]byte("Hello, World!")) for n := 0; n < b.N; n++ { setETag(c, true) } - utils.AssertEqual(b, `W/"13-1831710635"`, string(c.Fasthttp.Response.Header.Peek(HeaderETag))) + utils.AssertEqual(b, `W/"13-1831710635"`, string(c.Response().Header.Peek(HeaderETag))) } // go test -run Test_NewError @@ -799,16 +822,17 @@ func Test_NewError(t *testing.T) { func Test_Test_Timeout(t *testing.T) { app := New() - app.Settings.DisableStartupMessage = true + app.config.DisableStartupMessage = true - app.Get("/", func(_ *Ctx) {}) + app.Get("/", testEmptyHandler) resp, err := app.Test(httptest.NewRequest("GET", "/", nil), -1) utils.AssertEqual(t, nil, err, "app.Test(req)") utils.AssertEqual(t, 200, resp.StatusCode, "Status code") - app.Get("timeout", func(c *Ctx) { + app.Get("timeout", func(c *Ctx) error { time.Sleep(55 * time.Millisecond) + return nil }) _, err = app.Test(httptest.NewRequest("GET", "/timeout", nil), 50) @@ -823,9 +847,9 @@ func (errorReader) Read([]byte) (int, error) { func Test_Test_DumpError(t *testing.T) { app := New() - app.Settings.DisableStartupMessage = true + app.config.DisableStartupMessage = true - app.Get("/", func(_ *Ctx) {}) + app.Get("/", testEmptyHandler) resp, err := app.Test(httptest.NewRequest("GET", "/", errorReader(0))) utils.AssertEqual(t, true, resp == nil) @@ -844,24 +868,23 @@ func (invalidView) Load() error { return errors.New("invalid view") } func (i invalidView) Render(io.Writer, string, interface{}, ...string) error { panic("implement me") } func Test_App_Init_Error_View(t *testing.T) { - app := New(&Settings{Views: invalidView{}}) - app.init() + app := New(Config{Views: invalidView{}}) defer func() { if err := recover(); err != nil { utils.AssertEqual(t, "implement me", fmt.Sprintf("%v", err)) } }() - _ = app.Settings.Views.Render(nil, "", nil) + _ = app.config.Views.Render(nil, "", nil) } func Test_App_Stack(t *testing.T) { app := New() - app.Use("/path0", func(_ *Ctx) {}) - app.Get("/path1", func(_ *Ctx) {}) - app.Get("/path2", func(_ *Ctx) {}) - app.Post("/path3", func(_ *Ctx) {}) + app.Use("/path0", testEmptyHandler) + app.Get("/path1", testEmptyHandler) + app.Get("/path2", testEmptyHandler) + app.Post("/path3", testEmptyHandler) stack := app.Stack() utils.AssertEqual(t, 9, len(stack)) @@ -877,49 +900,49 @@ func Test_App_Stack(t *testing.T) { } // go test -run Test_App_ReadTimeout -//func Test_App_ReadTimeout(t *testing.T) { -// app := New(&Settings{ -// ReadTimeout: time.Nanosecond, -// IdleTimeout: time.Minute, -// DisableStartupMessage: true, -// DisableKeepalive: true, -// }) -// -// app.Get("/read-timeout", func(c *Ctx) { -// c.SendString("I should not be sent") -// }) -// -// go func() { -// time.Sleep(500 * time.Millisecond) -// -// conn, err := net.Dial("tcp4", "127.0.0.1:4004") -// utils.AssertEqual(t, nil, err) -// defer conn.Close() -// -// _, err = conn.Write([]byte("HEAD /read-timeout HTTP/1.1\r\n")) -// utils.AssertEqual(t, nil, err) -// -// buf := make([]byte, 1024) -// var n int -// n, err = conn.Read(buf) -// -// utils.AssertEqual(t, nil, err) -// utils.AssertEqual(t, true, bytes.Contains(buf[:n], []byte("408 Request Timeout"))) -// -// utils.AssertEqual(t, nil, app.Shutdown()) -// }() -// -// utils.AssertEqual(t, nil, app.Listen(4004)) -//} +func Test_App_ReadTimeout(t *testing.T) { + app := New(Config{ + ReadTimeout: time.Nanosecond, + IdleTimeout: time.Minute, + DisableStartupMessage: true, + DisableKeepalive: true, + }) + + app.Get("/read-timeout", func(c *Ctx) error { + return c.SendString("I should not be sent") + }) + + go func() { + time.Sleep(500 * time.Millisecond) + + conn, err := net.Dial("tcp4", "127.0.0.1:4004") + utils.AssertEqual(t, nil, err) + defer conn.Close() + + _, err = conn.Write([]byte("HEAD /read-timeout HTTP/1.1\r\n")) + utils.AssertEqual(t, nil, err) + + buf := make([]byte, 1024) + var n int + n, err = conn.Read(buf) + + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, bytes.Contains(buf[:n], []byte("408 Request Timeout"))) + + utils.AssertEqual(t, nil, app.Shutdown()) + }() + + utils.AssertEqual(t, nil, app.Listen(":4004")) +} // go test -run Test_App_BadRequest func Test_App_BadRequest(t *testing.T) { - app := New(&Settings{ + app := New(Config{ DisableStartupMessage: true, }) - app.Get("/bad-request", func(c *Ctx) { - c.SendString("I should not be sent") + app.Get("/bad-request", func(c *Ctx) error { + return c.SendString("I should not be sent") }) go func() { @@ -941,18 +964,18 @@ func Test_App_BadRequest(t *testing.T) { utils.AssertEqual(t, nil, app.Shutdown()) }() - utils.AssertEqual(t, nil, app.Listen(4005)) + utils.AssertEqual(t, nil, app.Listen(":4005")) } // go test -run Test_App_SmallReadBuffer func Test_App_SmallReadBuffer(t *testing.T) { - app := New(&Settings{ + app := New(Config{ ReadBufferSize: 1, DisableStartupMessage: true, }) - app.Get("/small-read-buffer", func(c *Ctx) { - c.SendString("I should not be sent") + app.Get("/small-read-buffer", func(c *Ctx) error { + return c.SendString("I should not be sent") }) go func() { @@ -965,5 +988,5 @@ func Test_App_SmallReadBuffer(t *testing.T) { utils.AssertEqual(t, nil, app.Shutdown()) }() - utils.AssertEqual(t, nil, app.Listen(4006)) + utils.AssertEqual(t, nil, app.Listen(":4006")) } diff --git a/ctx.go b/ctx.go index 7f737715..90280f77 100644 --- a/ctx.go +++ b/ctx.go @@ -6,13 +6,10 @@ package fiber import ( "bytes" - "context" - "encoding/json" "encoding/xml" "errors" "fmt" "io" - "log" "mime/multipart" "net/http" "path/filepath" @@ -22,12 +19,16 @@ import ( "text/template" "time" - utils "github.com/gofiber/utils" - schema "github.com/gorilla/schema" - bytebufferpool "github.com/valyala/bytebufferpool" - fasthttp "github.com/valyala/fasthttp" + "github.com/gofiber/fiber/v2/utils" + "github.com/gofiber/fiber/v2/utils/bytebufferpool" + "github.com/gofiber/fiber/v2/utils/encoding/json" + "github.com/gofiber/fiber/v2/utils/schema" + "github.com/valyala/fasthttp" ) +// maxParams defines the maximum number of parameters per route. +const maxParams = 30 + // Ctx represents the Context which hold the HTTP request and response. // It has methods for the request query string, parameters, body, HTTP headers and so on. type Ctx struct { @@ -37,16 +38,16 @@ type Ctx struct { indexHandler int // Index of the current handler method string // HTTP method methodINT int // HTTP method INT equivalent - path string // Prettified HTTP path + path string // Prettified HTTP path -> string copy from pathBuffer + pathBuffer []byte // Prettified HTTP path buffer treePath string // Path for the search in the tree pathOriginal string // Original HTTP path - values []string // Route parameter values - err error // Contains error if passed to Next - Fasthttp *fasthttp.RequestCtx // Reference to *fasthttp.RequestCtx + values [maxParams]string // Route parameter values + fasthttp *fasthttp.RequestCtx // Reference to *fasthttp.RequestCtx matched bool // Non use route matched } -// Range data for ctx.Range +// Range data for c.Range type Range struct { Type string Ranges []struct { @@ -55,7 +56,7 @@ type Range struct { } } -// Cookie data for ctx.Cookie +// Cookie data for c.Cookie type Cookie struct { Name string `json:"name"` Value string `json:"value"` @@ -75,43 +76,41 @@ type Views interface { // AcquireCtx retrieves a new Ctx from the pool. func (app *App) AcquireCtx(fctx *fasthttp.RequestCtx) *Ctx { - ctx := app.pool.Get().(*Ctx) + c := app.pool.Get().(*Ctx) // Set app reference - ctx.app = app + c.app = app // Reset route and handler index - ctx.indexRoute = -1 - ctx.indexHandler = 0 + c.indexRoute = -1 + c.indexHandler = 0 // Reset matched flag - ctx.matched = false + c.matched = false // Set paths - ctx.path = getString(fctx.URI().PathOriginal()) - ctx.pathOriginal = ctx.path + c.pathBuffer = append(c.pathBuffer[0:0], fctx.URI().PathOriginal()...) + c.pathOriginal = getString(fctx.URI().PathOriginal()) // Set method - ctx.method = getString(fctx.Request.Header.Method()) - ctx.methodINT = methodInt(ctx.method) + c.method = getString(fctx.Request.Header.Method()) + c.methodINT = methodInt(c.method) // Attach *fasthttp.RequestCtx to ctx - ctx.Fasthttp = fctx + c.fasthttp = fctx // Prettify path - ctx.prettifyPath() - return ctx + c.prettifyPath() + return c } // ReleaseCtx releases the ctx back into the pool. -func (app *App) ReleaseCtx(ctx *Ctx) { +func (app *App) ReleaseCtx(c *Ctx) { // Reset values - ctx.route = nil - ctx.values = nil - ctx.Fasthttp = nil - ctx.err = nil - app.pool.Put(ctx) + c.route = nil + c.fasthttp = nil + app.pool.Put(c) } // Accepts checks if the specified extensions or content types are acceptable. -func (ctx *Ctx) Accepts(offers ...string) string { +func (c *Ctx) Accepts(offers ...string) string { if len(offers) == 0 { return "" } - header := ctx.Get(HeaderAccept) + header := c.Get(HeaderAccept) if header == "" { return offers[0] } @@ -149,32 +148,32 @@ func (ctx *Ctx) Accepts(offers ...string) string { } // AcceptsCharsets checks if the specified charset is acceptable. -func (ctx *Ctx) AcceptsCharsets(offers ...string) string { - return getOffer(ctx.Get(HeaderAcceptCharset), offers...) +func (c *Ctx) AcceptsCharsets(offers ...string) string { + return getOffer(c.Get(HeaderAcceptCharset), offers...) } // AcceptsEncodings checks if the specified encoding is acceptable. -func (ctx *Ctx) AcceptsEncodings(offers ...string) string { - return getOffer(ctx.Get(HeaderAcceptEncoding), offers...) +func (c *Ctx) AcceptsEncodings(offers ...string) string { + return getOffer(c.Get(HeaderAcceptEncoding), offers...) } // AcceptsLanguages checks if the specified language is acceptable. -func (ctx *Ctx) AcceptsLanguages(offers ...string) string { - return getOffer(ctx.Get(HeaderAcceptLanguage), offers...) +func (c *Ctx) AcceptsLanguages(offers ...string) string { + return getOffer(c.Get(HeaderAcceptLanguage), offers...) } -// App returns the *App reference to access Settings or ErrorHandler -func (ctx *Ctx) App() *App { - return ctx.app +// App returns the *App reference to the instance of the Fiber application +func (c *Ctx) App() *App { + return c.app } // Append the specified value to the HTTP response header field. // If the header is not already set, it creates the header with the specified value. -func (ctx *Ctx) Append(field string, values ...string) { +func (c *Ctx) Append(field string, values ...string) { if len(values) == 0 { return } - h := getString(ctx.Fasthttp.Response.Header.Peek(field)) + h := getString(c.fasthttp.Response.Header.Peek(field)) originalH := h for _, value := range values { if len(h) == 0 { @@ -185,34 +184,34 @@ func (ctx *Ctx) Append(field string, values ...string) { } } if originalH != h { - ctx.Set(field, h) + c.Set(field, h) } } // Attachment sets the HTTP response Content-Disposition header field to attachment. -func (ctx *Ctx) Attachment(filename ...string) { +func (c *Ctx) Attachment(filename ...string) { if len(filename) > 0 { fname := filepath.Base(filename[0]) - ctx.Type(filepath.Ext(fname)) + c.Type(filepath.Ext(fname)) - ctx.Set(HeaderContentDisposition, `attachment; filename="`+quoteString(fname)+`"`) + c.setCanonical(HeaderContentDisposition, `attachment; filename="`+quoteString(fname)+`"`) return } - ctx.Set(HeaderContentDisposition, "attachment") + c.setCanonical(HeaderContentDisposition, "attachment") } // BaseURL returns (protocol + host + base path). -func (ctx *Ctx) BaseURL() string { +func (c *Ctx) BaseURL() string { // TODO: Could be improved: 53.8 ns/op 32 B/op 1 allocs/op // Should work like https://codeigniter.com/user_guide/helpers/url_helper.html - return ctx.Protocol() + "://" + ctx.Hostname() + return c.Protocol() + "://" + c.Hostname() } // Body contains the raw body submitted in a POST request. // Returned value is only valid within the handler. Do not store any references. // Make copies or use the Immutable setting instead. -func (ctx *Ctx) Body() string { - return getString(ctx.Fasthttp.Request.Body()) +func (c *Ctx) Body() []byte { + return c.fasthttp.Request.Body() } // decoderPool helps to improve BodyParser's and QueryParser's performance @@ -225,89 +224,62 @@ var decoderPool = &sync.Pool{New: func() interface{} { // BodyParser binds the request body to a struct. // It supports decoding the following content types based on the Content-Type header: // application/json, application/xml, application/x-www-form-urlencoded, multipart/form-data -func (ctx *Ctx) BodyParser(out interface{}) error { +func (c *Ctx) BodyParser(out interface{}) error { // Get decoder from pool schemaDecoder := decoderPool.Get().(*schema.Decoder) defer decoderPool.Put(schemaDecoder) // Get content-type - ctype := getString(ctx.Fasthttp.Request.Header.ContentType()) + ctype := getString(c.fasthttp.Request.Header.ContentType()) // Parse body accordingly if strings.HasPrefix(ctype, MIMEApplicationJSON) { schemaDecoder.SetAliasTag("json") - return json.Unmarshal(ctx.Fasthttp.Request.Body(), out) + return json.Unmarshal(c.fasthttp.Request.Body(), out) } else if strings.HasPrefix(ctype, MIMEApplicationForm) { schemaDecoder.SetAliasTag("form") data := make(map[string][]string) - ctx.Fasthttp.PostArgs().VisitAll(func(key []byte, val []byte) { + c.fasthttp.PostArgs().VisitAll(func(key []byte, val []byte) { data[getString(key)] = append(data[getString(key)], getString(val)) }) return schemaDecoder.Decode(out, data) } else if strings.HasPrefix(ctype, MIMEMultipartForm) { schemaDecoder.SetAliasTag("form") - data, err := ctx.Fasthttp.MultipartForm() + data, err := c.fasthttp.MultipartForm() if err != nil { return err } return schemaDecoder.Decode(out, data.Value) } else if strings.HasPrefix(ctype, MIMETextXML) || strings.HasPrefix(ctype, MIMEApplicationXML) { schemaDecoder.SetAliasTag("xml") - return xml.Unmarshal(ctx.Fasthttp.Request.Body(), out) + return xml.Unmarshal(c.fasthttp.Request.Body(), out) } - - // Query params in BodyParser is deprecated - if ctx.Fasthttp.QueryArgs().Len() > 0 { - fmt.Println("bodyparser: parsing query strings is deprecated since v1.12.7, please use `ctx.QueryParser` instead") - return ctx.QueryParser(out) - } - // No suitable content type found return fmt.Errorf("bodyparser: cannot parse content-type: %v", ctype) } -// QueryParser binds the query string to a struct. -func (ctx *Ctx) QueryParser(out interface{}) error { - if ctx.Fasthttp.QueryArgs().Len() < 1 { - return nil - } - // Get decoder from pool - var decoder = decoderPool.Get().(*schema.Decoder) - defer decoderPool.Put(decoder) - - // Set correct alias tag - decoder.SetAliasTag("query") - - data := make(map[string][]string) - ctx.Fasthttp.QueryArgs().VisitAll(func(key []byte, val []byte) { - data[getString(key)] = append(data[getString(key)], getString(val)) - }) - - return decoder.Decode(out, data) -} - // ClearCookie expires a specific cookie by key on the client side. // If no key is provided it expires all cookies that came with the request. -func (ctx *Ctx) ClearCookie(key ...string) { +func (c *Ctx) ClearCookie(key ...string) { if len(key) > 0 { for i := range key { - ctx.Fasthttp.Response.Header.DelClientCookie(key[i]) + c.fasthttp.Response.Header.DelClientCookie(key[i]) } return } - ctx.Fasthttp.Request.Header.VisitAllCookie(func(k, v []byte) { - ctx.Fasthttp.Response.Header.DelClientCookieBytes(k) + c.fasthttp.Request.Header.VisitAllCookie(func(k, v []byte) { + c.fasthttp.Response.Header.DelClientCookieBytes(k) }) } -// Context returns context.Context that carries a deadline, a cancellation signal, -// and other values across API boundaries. -func (ctx *Ctx) Context() context.Context { - return ctx.Fasthttp +// Context returns *fasthttp.RequestCtx that carries a deadline +// a cancellation signal, and other values across API boundaries. +func (c *Ctx) Context() *fasthttp.RequestCtx { + return c.fasthttp } // Cookie sets a cookie by passing a cookie struct. -func (ctx *Ctx) Cookie(cookie *Cookie) { +func (c *Ctx) Cookie(cookie *Cookie) { fcookie := fasthttp.AcquireCookie() fcookie.SetKey(cookie.Name) fcookie.SetValue(cookie.Value) @@ -326,7 +298,7 @@ func (ctx *Ctx) Cookie(cookie *Cookie) { fcookie.SetSameSite(fasthttp.CookieSameSiteLaxMode) } - ctx.Fasthttp.Response.Header.SetCookie(fcookie) + c.fasthttp.Response.Header.SetCookie(fcookie) fasthttp.ReleaseCookie(fcookie) } @@ -335,37 +307,47 @@ func (ctx *Ctx) Cookie(cookie *Cookie) { // If a default value is given, it will return that value if the cookie doesn't exist. // The returned value is only valid within the handler. Do not store any references. // Make copies or use the Immutable setting to use the value outside the Handler. -func (ctx *Ctx) Cookies(key string, defaultValue ...string) string { - return defaultString(getString(ctx.Fasthttp.Request.Header.Cookie(key)), defaultValue) +func (c *Ctx) Cookies(key string, defaultValue ...string) string { + return defaultString(getString(c.fasthttp.Request.Header.Cookie(key)), defaultValue) } // Download transfers the file from path as an attachment. // Typically, browsers will prompt the user for download. // By default, the Content-Disposition header filename= parameter is the filepath (this typically appears in the browser dialog). // Override this default with the filename parameter. -func (ctx *Ctx) Download(file string, filename ...string) error { - fname := filepath.Base(file) +func (c *Ctx) Download(file string, filename ...string) error { + var fname string if len(filename) > 0 { fname = filename[0] + } else { + fname = filepath.Base(file) } - ctx.Set(HeaderContentDisposition, `attachment; filename="`+quoteString(fname)+`"`) - return ctx.SendFile(file) + c.setCanonical(HeaderContentDisposition, `attachment; filename="`+quoteString(fname)+`"`) + return c.SendFile(file) } -// Error contains the error information passed via the Next(err) method. -func (ctx *Ctx) Error() error { - return ctx.err +// Request return the *fasthttp.Request object +// This allows you to use all fasthttp request methods +// https://godoc.org/github.com/valyala/fasthttp#Request +func (c *Ctx) Request() *fasthttp.Request { + return &c.fasthttp.Request +} + +// Request return the *fasthttp.Response object +// This allows you to use all fasthttp response methods +// https://godoc.org/github.com/valyala/fasthttp#Response +func (c *Ctx) Response() *fasthttp.Response { + return &c.fasthttp.Response } // Format performs content-negotiation on the Accept HTTP header. // It uses Accepts to select a proper format. // If the header is not specified or there is no proper format, text/plain is used. -func (ctx *Ctx) Format(body interface{}) { +func (c *Ctx) Format(body interface{}) error { // Get accepted content type - accept := ctx.Accepts("html", "json", "txt", "xml") + accept := c.Accepts("html", "json", "txt", "xml") // Set accepted content type - ctx.Type(accept) - + c.Type(accept) // Type convert provided body var b string switch val := body.(type) { @@ -380,37 +362,32 @@ func (ctx *Ctx) Format(body interface{}) { // Format based on the accept content type switch accept { case "html": - ctx.SendString("

" + b + "

") + return c.SendString("

" + b + "

") case "json": - if err := ctx.JSON(body); err != nil { - ctx.Send(body) // Fallback - log.Println("Format: error serializing json ", err) - } + return c.JSON(body) case "txt": - ctx.SendString(b) + return c.SendString(b) case "xml": raw, err := xml.Marshal(body) if err != nil { - ctx.Send(body) // Fallback - log.Println("Format: error serializing xml ", err) - } else { - ctx.Fasthttp.Response.SetBodyRaw(raw) + return fmt.Errorf("error serializing xml: %v", body) } - default: - ctx.SendString(b) + c.fasthttp.Response.SetBody(raw) + return nil } + return c.SendString(b) } // FormFile returns the first file by key from a MultipartForm. -func (ctx *Ctx) FormFile(key string) (*multipart.FileHeader, error) { - return ctx.Fasthttp.FormFile(key) +func (c *Ctx) FormFile(key string) (*multipart.FileHeader, error) { + return c.fasthttp.FormFile(key) } // FormValue returns the first value by key from a MultipartForm. // Returned value is only valid within the handler. Do not store any references. // Make copies or use the Immutable setting instead. -func (ctx *Ctx) FormValue(key string) (value string) { - return getString(ctx.Fasthttp.FormValue(key)) +func (c *Ctx) FormValue(key string, defaultValue ...string) string { + return defaultString(getString(c.fasthttp.FormValue(key)), defaultValue) } // Fresh returns true when the response is still โ€œfreshโ€ in the client's cache, @@ -419,10 +396,10 @@ func (ctx *Ctx) FormValue(key string) (value string) { // When a client sends the Cache-Control: no-cache request header to indicate an end-to-end // reload request, this module will return false to make handling these requests transparent. // https://github.com/jshttp/fresh/blob/10e0471669dbbfbfd8de65bc6efac2ddd0bfa057/index.js#L33 -func (ctx *Ctx) Fresh() bool { +func (c *Ctx) Fresh() bool { // fields - var modifiedSince = ctx.Get(HeaderIfModifiedSince) - var noneMatch = ctx.Get(HeaderIfNoneMatch) + var modifiedSince = c.Get(HeaderIfModifiedSince) + var noneMatch = c.Get(HeaderIfNoneMatch) // unconditional request if modifiedSince == "" && noneMatch == "" { @@ -432,14 +409,14 @@ func (ctx *Ctx) Fresh() bool { // Always return stale when Cache-Control: no-cache // to support end-to-end reload requests // https://tools.ietf.org/html/rfc2616#section-14.9.4 - cacheControl := ctx.Get(HeaderCacheControl) + cacheControl := c.Get(HeaderCacheControl) if cacheControl != "" && isNoCache(cacheControl) { return false } // if-none-match if noneMatch != "" && noneMatch != "*" { - var etag = getString(ctx.Fasthttp.Response.Header.Peek(HeaderETag)) + var etag = getString(c.fasthttp.Response.Header.Peek(HeaderETag)) if etag == "" { return false } @@ -448,7 +425,7 @@ func (ctx *Ctx) Fresh() bool { } if modifiedSince != "" { - var lastModified = getString(ctx.Fasthttp.Response.Header.Peek(HeaderLastModified)) + var lastModified = getString(c.fasthttp.Response.Header.Peek(HeaderLastModified)) if lastModified != "" { lastModifiedTime, err := http.ParseTime(lastModified) if err != nil { @@ -469,25 +446,28 @@ func (ctx *Ctx) Fresh() bool { // Field names are case-insensitive // Returned value is only valid within the handler. Do not store any references. // Make copies or use the Immutable setting instead. -func (ctx *Ctx) Get(key string, defaultValue ...string) string { - return defaultString(getString(ctx.Fasthttp.Request.Header.Peek(key)), defaultValue) +func (c *Ctx) Get(key string, defaultValue ...string) string { + return defaultString(getString(c.fasthttp.Request.Header.Peek(key)), defaultValue) } // Hostname contains the hostname derived from the Host HTTP header. // Returned value is only valid within the handler. Do not store any references. // Make copies or use the Immutable setting instead. -func (ctx *Ctx) Hostname() string { - return getString(ctx.Fasthttp.URI().Host()) +func (c *Ctx) Hostname() string { + return getString(c.fasthttp.Request.URI().Host()) } // IP returns the remote IP address of the request. -func (ctx *Ctx) IP() string { - return ctx.Fasthttp.RemoteIP().String() +func (c *Ctx) IP() string { + if len(c.app.config.ProxyHeader) > 0 { + return c.Get(c.app.config.ProxyHeader) + } + return c.fasthttp.RemoteIP().String() } // IPs returns an string slice of IP addresses specified in the X-Forwarded-For request header. -func (ctx *Ctx) IPs() (ips []string) { - header := ctx.Fasthttp.Request.Header.Peek(HeaderXForwardedFor) +func (c *Ctx) IPs() (ips []string) { + header := c.fasthttp.Request.Header.Peek(HeaderXForwardedFor) if len(header) == 0 { return } @@ -496,10 +476,10 @@ func (ctx *Ctx) IPs() (ips []string) { for { commaPos = bytes.IndexByte(header, ',') if commaPos != -1 { - ips[i] = getString(header[:commaPos]) - header, i = header[commaPos+2:], i+1 + ips[i] = utils.Trim(getString(header[:commaPos]), ' ') + header, i = header[commaPos+1:], i+1 } else { - ips[i] = getString(header) + ips[i] = utils.Trim(getString(header), ' ') return } } @@ -507,37 +487,34 @@ func (ctx *Ctx) IPs() (ips []string) { // Is returns the matching content type, // if the incoming request's Content-Type HTTP header field matches the MIME type specified by the type parameter -func (ctx *Ctx) Is(extension string) bool { +func (c *Ctx) Is(extension string) bool { extensionHeader := utils.GetMIME(extension) if extensionHeader == "" { return false } return strings.HasPrefix( - utils.TrimLeft(utils.GetString(ctx.Fasthttp.Request.Header.ContentType()), ' '), + utils.TrimLeft(utils.GetString(c.fasthttp.Request.Header.ContentType()), ' '), extensionHeader, ) } // JSON converts any interface or string to JSON. // This method also sets the content header to application/json. -func (ctx *Ctx) JSON(data interface{}) error { +func (c *Ctx) JSON(data interface{}) error { raw, err := json.Marshal(data) - // Check for errors if err != nil { return err } - // Set http headers - ctx.Fasthttp.Response.Header.SetContentType(MIMEApplicationJSON) - ctx.Fasthttp.Response.SetBodyRaw(raw) - + c.fasthttp.Response.SetBodyRaw(raw) + c.fasthttp.Response.Header.SetContentType(MIMEApplicationJSON) return nil } // JSONP sends a JSON response with JSONP support. // This method is identical to JSON, except that it opts-in to JSONP callback support. // By default, the callback name is simply callback. -func (ctx *Ctx) JSONP(data interface{}, callback ...string) error { +func (c *Ctx) JSONP(data interface{}, callback ...string) error { raw, err := json.Marshal(data) if err != nil { @@ -554,15 +531,13 @@ func (ctx *Ctx) JSONP(data interface{}, callback ...string) error { result = cb + "(" + getString(raw) + ");" - ctx.Set(HeaderXContentTypeOptions, "nosniff") - ctx.Fasthttp.Response.Header.SetContentType(MIMEApplicationJavaScriptCharsetUTF8) - ctx.SendString(result) - - return nil + c.setCanonical(HeaderXContentTypeOptions, "nosniff") + c.fasthttp.Response.Header.SetContentType(MIMEApplicationJavaScriptCharsetUTF8) + return c.SendString(result) } // Links joins the links followed by the property to populate the response's Link HTTP header field. -func (ctx *Ctx) Links(link ...string) { +func (c *Ctx) Links(link ...string) { if len(link) == 0 { return } @@ -576,71 +551,70 @@ func (ctx *Ctx) Links(link ...string) { _, _ = bb.WriteString(`; rel="` + link[i] + `",`) } } - ctx.Set(HeaderLink, utils.TrimRight(getString(bb.Bytes()), ',')) + c.setCanonical(HeaderLink, utils.TrimRight(getString(bb.Bytes()), ',')) bytebufferpool.Put(bb) } // Locals makes it possible to pass interface{} values under string keys scoped to the request // and therefore available to all following routes that match the request. -func (ctx *Ctx) Locals(key string, value ...interface{}) (val interface{}) { +func (c *Ctx) Locals(key string, value ...interface{}) (val interface{}) { if len(value) == 0 { - return ctx.Fasthttp.UserValue(key) + return c.fasthttp.UserValue(key) } - ctx.Fasthttp.SetUserValue(key, value[0]) + c.fasthttp.SetUserValue(key, value[0]) return value[0] } // Location sets the response Location HTTP header to the specified path parameter. -func (ctx *Ctx) Location(path string) { - ctx.Set(HeaderLocation, path) +func (c *Ctx) Location(path string) { + c.setCanonical(HeaderLocation, path) } // Method contains a string corresponding to the HTTP method of the request: GET, POST, PUT and so on. -func (ctx *Ctx) Method(override ...string) string { +func (c *Ctx) Method(override ...string) string { if len(override) > 0 { method := utils.ToUpper(override[0]) mINT := methodInt(method) if mINT == -1 { - return ctx.method + return c.method } - ctx.method = method - ctx.methodINT = mINT + c.method = method + c.methodINT = mINT } - return ctx.method + return c.method } // MultipartForm parse form entries from binary. // This returns a map[string][]string, so given a key the value will be a string slice. -func (ctx *Ctx) MultipartForm() (*multipart.Form, error) { - return ctx.Fasthttp.MultipartForm() +func (c *Ctx) MultipartForm() (*multipart.Form, error) { + return c.fasthttp.MultipartForm() } // Next executes the next method in the stack that matches the current route. -// You can pass an optional error for custom error handling. -func (ctx *Ctx) Next(err ...error) { - if len(err) > 0 { - ctx.err = err[0] - ctx.app.Settings.ErrorHandler(ctx, ctx.err) - return - } - +func (c *Ctx) Next() (err error) { // Increment handler index - ctx.indexHandler++ + c.indexHandler++ // Did we executed all route handlers? - if ctx.indexHandler < len(ctx.route.Handlers) { + if c.indexHandler < len(c.route.Handlers) { // Continue route stack - ctx.route.Handlers[ctx.indexHandler](ctx) + if err = c.route.Handlers[c.indexHandler](c); err != nil { + if err = c.app.config.ErrorHandler(c, err); err != nil { + _ = c.SendStatus(StatusInternalServerError) + } + return err + } } else { // Continue handler stack - ctx.app.next(ctx) + _, err = c.app.next(c) } + return } // OriginalURL contains the original request URL. // Returned value is only valid within the handler. Do not store any references. // Make copies or use the Immutable setting to use the value outside the Handler. -func (ctx *Ctx) OriginalURL() string { - return getString(ctx.Fasthttp.Request.Header.RequestURI()) +func (c *Ctx) OriginalURL() string { + return getString(c.fasthttp.Request.Header.RequestURI()) } // Params is used to get the route parameters. @@ -648,20 +622,20 @@ func (ctx *Ctx) OriginalURL() string { // If a default value is given, it will return that value if the param doesn't exist. // Returned value is only valid within the handler. Do not store any references. // Make copies or use the Immutable setting to use the value outside the Handler. -func (ctx *Ctx) Params(key string, defaultValue ...string) string { +func (c *Ctx) Params(key string, defaultValue ...string) string { if key == "*" || key == "+" { key += "1" } - for i := range ctx.route.Params { - if len(key) != len(ctx.route.Params[i]) { + for i := range c.route.Params { + if len(key) != len(c.route.Params[i]) { continue } - if ctx.route.Params[i] == key { + if c.route.Params[i] == key { // in case values are not here - if len(ctx.values) <= i || len(ctx.values[i]) == 0 { + if len(c.values) <= i || len(c.values[i]) == 0 { break } - return ctx.values[i] + return c.values[i] } } return defaultString("", defaultValue) @@ -669,26 +643,29 @@ func (ctx *Ctx) Params(key string, defaultValue ...string) string { // Path returns the path part of the request URL. // Optionally, you could override the path. -func (ctx *Ctx) Path(override ...string) string { - if len(override) != 0 && ctx.path != override[0] { +func (c *Ctx) Path(override ...string) string { + if len(override) != 0 && c.path != override[0] { // Set new path to context - ctx.path = override[0] - ctx.pathOriginal = ctx.path + c.pathBuffer = append(c.pathBuffer[0:0], override[0]...) + c.pathOriginal = override[0] + // c.path = override[0] + // c.pathOriginal = c.path + // Set new path to request context - ctx.Fasthttp.Request.URI().SetPath(ctx.pathOriginal) + c.fasthttp.Request.URI().SetPath(c.pathOriginal) // Prettify path - ctx.prettifyPath() + c.prettifyPath() } - return ctx.pathOriginal + return c.pathOriginal } // Protocol contains the request protocol string: http or https for TLS requests. -func (ctx *Ctx) Protocol() string { - if ctx.Fasthttp.IsTLS() { +func (c *Ctx) Protocol() string { + if c.fasthttp.IsTLS() { return "https" } scheme := "http" - ctx.Fasthttp.Request.Header.VisitAll(func(key, val []byte) { + c.fasthttp.Request.Header.VisitAll(func(key, val []byte) { if len(key) < 12 { return // X-Forwarded- } else if bytes.HasPrefix(key, []byte("X-Forwarded-")) { @@ -711,8 +688,28 @@ func (ctx *Ctx) Protocol() string { // If a default value is given, it will return that value if the query doesn't exist. // Returned value is only valid within the handler. Do not store any references. // Make copies or use the Immutable setting to use the value outside the Handler. -func (ctx *Ctx) Query(key string, defaultValue ...string) string { - return defaultString(getString(ctx.Fasthttp.QueryArgs().Peek(key)), defaultValue) +func (c *Ctx) Query(key string, defaultValue ...string) string { + return defaultString(getString(c.fasthttp.QueryArgs().Peek(key)), defaultValue) +} + +// QueryParser binds the query string to a struct. +func (c *Ctx) QueryParser(out interface{}) error { + if c.fasthttp.QueryArgs().Len() < 1 { + return nil + } + // Get decoder from pool + var decoder = decoderPool.Get().(*schema.Decoder) + defer decoderPool.Put(decoder) + + // Set correct alias tag + decoder.SetAliasTag("query") + + data := make(map[string][]string) + c.fasthttp.QueryArgs().VisitAll(func(key []byte, val []byte) { + data[getString(key)] = append(data[getString(key)], getString(val)) + }) + + return decoder.Decode(out, data) } var ( @@ -721,8 +718,8 @@ var ( ) // Range returns a struct containing the type and a slice of ranges. -func (ctx *Ctx) Range(size int) (rangeData Range, err error) { - rangeStr := ctx.Get(HeaderRange) +func (c *Ctx) Range(size int) (rangeData Range, err error) { + rangeStr := c.Get(HeaderRange) if rangeStr == "" || !strings.Contains(rangeStr, "=") { err = ErrRangeMalformed return @@ -772,25 +769,27 @@ func (ctx *Ctx) Range(size int) (rangeData Range, err error) { // Redirect to the URL derived from the specified path, with specified status. // If status is not specified, status defaults to 302 Found. -func (ctx *Ctx) Redirect(location string, status ...int) { - ctx.Set(HeaderLocation, location) +func (c *Ctx) Redirect(location string, status ...int) error { + c.setCanonical(HeaderLocation, location) if len(status) > 0 { - ctx.Status(status[0]) + c.Status(status[0]) } else { - ctx.Status(StatusFound) + c.Status(StatusFound) } + return nil } // Render a template with data and sends a text/html response. // We support the following engines: html, amber, handlebars, mustache, pug -func (ctx *Ctx) Render(name string, bind interface{}, layouts ...string) (err error) { +func (c *Ctx) Render(name string, bind interface{}, layouts ...string) error { + var err error // Get new buffer from pool buf := bytebufferpool.Get() defer bytebufferpool.Put(buf) - if ctx.app.Settings.Views != nil { + if c.app.config.Views != nil { // Render template from Views - if err := ctx.app.Settings.Views.Render(buf, name, bind, layouts...); err != nil { + if err := c.app.config.Views.Render(buf, name, bind, layouts...); err != nil { return err } } else { @@ -810,67 +809,62 @@ func (ctx *Ctx) Render(name string, bind interface{}, layouts ...string) (err er } } // Set Content-Type to text/html - ctx.Fasthttp.Response.Header.SetContentType(MIMETextHTMLCharsetUTF8) + c.fasthttp.Response.Header.SetContentType(MIMETextHTMLCharsetUTF8) // Set rendered template to body - ctx.Fasthttp.Response.SetBody(buf.Bytes()) + c.fasthttp.Response.SetBody(buf.Bytes()) // Return err if exist - return + return err } // Route returns the matched Route struct. -func (ctx *Ctx) Route() *Route { - if ctx.route == nil { +func (c *Ctx) Route() *Route { + if c.route == nil { // Fallback for fasthttp error handler return &Route{ - path: ctx.pathOriginal, - Path: ctx.pathOriginal, - Method: ctx.method, + path: c.pathOriginal, + Path: c.pathOriginal, + Method: c.method, Handlers: make([]Handler, 0), + Params: make([]string, 0), } } - return ctx.route + return c.route } // SaveFile saves any multipart file to disk. -func (ctx *Ctx) SaveFile(fileheader *multipart.FileHeader, path string) error { +func (c *Ctx) SaveFile(fileheader *multipart.FileHeader, path string) error { return fasthttp.SaveMultipartFile(fileheader, path) } // Secure returns a boolean property, that is true, if a TLS connection is established. -func (ctx *Ctx) Secure() bool { - return ctx.Fasthttp.IsTLS() +func (c *Ctx) Secure() bool { + return c.fasthttp.IsTLS() } -// Send sets the HTTP response body. The input can be of any type, io.Reader is also supported. -func (ctx *Ctx) Send(bodies ...interface{}) { - // Reset response body - ctx.SendString("") - // Write response body - ctx.Write(bodies...) -} - -// SendBytes sets the HTTP response body for []byte types without copying it. +// Send sets the HTTP response body without copying it. // From this point onward the body argument must not be changed. -func (ctx *Ctx) SendBytes(body []byte) { - ctx.Fasthttp.Response.SetBodyRaw(body) +func (c *Ctx) Send(body []byte) error { + // Write response body + c.fasthttp.Response.SetBodyRaw(body) + return nil } -var fsOnce sync.Once +var sendFileOnce sync.Once var sendFileFS *fasthttp.FS var sendFileHandler fasthttp.RequestHandler // SendFile transfers the file from the given path. // The file is not compressed by default, enable this by passing a 'true' argument // Sets the Content-Type response HTTP header field based on the filenames extension. -func (ctx *Ctx) SendFile(file string, compress ...bool) error { +func (c *Ctx) SendFile(file string, compress ...bool) error { // https://github.com/valyala/fasthttp/blob/master/fs.go#L81 - fsOnce.Do(func() { + sendFileOnce.Do(func() { sendFileFS = &fasthttp.FS{ Root: "/", GenerateIndexPages: false, AcceptByteRange: true, Compress: true, - CompressedFileSuffix: ctx.app.Settings.CompressedFileSuffix, + CompressedFileSuffix: c.app.config.CompressedFileSuffix, CacheDuration: 10 * time.Second, IndexNames: []string{"index.html"}, PathNotFound: func(ctx *fasthttp.RequestCtx) { @@ -881,11 +875,11 @@ func (ctx *Ctx) SendFile(file string, compress ...bool) error { }) // Keep original path for mutable params - ctx.pathOriginal = utils.ImmutableString(ctx.pathOriginal) + c.pathOriginal = utils.ImmutableString(c.pathOriginal) // Disable compression if len(compress) <= 0 || !compress[0] { // https://github.com/valyala/fasthttp/blob/master/fs.go#L46 - ctx.Fasthttp.Request.Header.Del(HeaderAcceptEncoding) + c.fasthttp.Request.Header.Del(HeaderAcceptEncoding) } // https://github.com/valyala/fasthttp/blob/master/fs.go#L85 if len(file) == 0 || file[0] != '/' { @@ -899,16 +893,16 @@ func (ctx *Ctx) SendFile(file string, compress ...bool) error { } } // Set new URI for fileHandler - ctx.Fasthttp.Request.SetRequestURI(file) + c.fasthttp.Request.SetRequestURI(file) // Save status code - status := ctx.Fasthttp.Response.StatusCode() + status := c.fasthttp.Response.StatusCode() // Serve file - sendFileHandler(ctx.Fasthttp) + sendFileHandler(c.fasthttp) // Get the status code which is set by fasthttp - fsStatus := ctx.Fasthttp.Response.StatusCode() + fsStatus := c.fasthttp.Response.StatusCode() // Set the status code set by the user if it is different from the fasthttp status code and 200 if status != fsStatus && status != StatusOK { - ctx.Status(status) + c.Status(status) } // Check for error if status != StatusNotFound && fsStatus == StatusNotFound { @@ -919,43 +913,55 @@ func (ctx *Ctx) SendFile(file string, compress ...bool) error { // SendStatus sets the HTTP status code and if the response body is empty, // it sets the correct status message in the body. -func (ctx *Ctx) SendStatus(status int) { - ctx.Status(status) +func (c *Ctx) SendStatus(status int) error { + c.Status(status) + // Only set status body when there is no response body - if len(ctx.Fasthttp.Response.Body()) == 0 { - ctx.SendString(utils.StatusMessage(status)) + if len(c.fasthttp.Response.Body()) == 0 { + return c.SendString(utils.StatusMessage(status)) } + + return nil } // SendString sets the HTTP response body for string types. // This means no type assertion, recommended for faster performance -func (ctx *Ctx) SendString(body string) { - ctx.Fasthttp.Response.SetBodyString(body) +func (c *Ctx) SendString(body string) error { + c.fasthttp.Response.SetBodyString(body) + + return nil } // SendStream sets response body stream and optional body size. -func (ctx *Ctx) SendStream(stream io.Reader, size ...int) { +func (c *Ctx) SendStream(stream io.Reader, size ...int) error { if len(size) > 0 && size[0] >= 0 { - ctx.Fasthttp.Response.SetBodyStream(stream, size[0]) + c.fasthttp.Response.SetBodyStream(stream, size[0]) } else { - ctx.Fasthttp.Response.SetBodyStream(stream, -1) - ctx.Set(HeaderContentLength, strconv.Itoa(len(ctx.Fasthttp.Response.Body()))) + c.fasthttp.Response.SetBodyStream(stream, -1) + c.setCanonical(HeaderContentLength, strconv.Itoa(len(c.fasthttp.Response.Body()))) } + + return nil } // Set sets the response's HTTP header field to the specified key, value. -func (ctx *Ctx) Set(key string, val string) { - ctx.Fasthttp.Response.Header.Set(key, val) +func (c *Ctx) Set(key string, val string) { + c.fasthttp.Response.Header.Set(key, removeNewLines(val)) + //c.fasthttp.Response.Header.Set(key, val) +} + +func (c *Ctx) setCanonical(key string, val string) { + c.fasthttp.Response.Header.SetCanonical(utils.GetBytes(key), utils.GetBytes(val)) } // Subdomains returns a string slice of subdomains in the domain name of the request. // The subdomain offset, which defaults to 2, is used for determining the beginning of the subdomain segments. -func (ctx *Ctx) Subdomains(offset ...int) []string { +func (c *Ctx) Subdomains(offset ...int) []string { o := 2 if len(offset) > 0 { o = offset[0] } - subdomains := strings.Split(ctx.Hostname(), ".") + subdomains := strings.Split(c.Hostname(), ".") l := len(subdomains) - o // Check index to avoid slice bounds out of range panic if l < 0 { @@ -966,80 +972,77 @@ func (ctx *Ctx) Subdomains(offset ...int) []string { } // Stale is not implemented yet, pull requests are welcome! -func (ctx *Ctx) Stale() bool { - return !ctx.Fresh() +func (c *Ctx) Stale() bool { + return !c.Fresh() } // Status sets the HTTP status for the response. // This method is chainable. -func (ctx *Ctx) Status(status int) *Ctx { - ctx.Fasthttp.Response.SetStatusCode(status) - return ctx +func (c *Ctx) Status(status int) *Ctx { + c.fasthttp.Response.SetStatusCode(status) + return c +} + +// String returns unique string representation of the ctx. +// +// The returned value may be useful for logging. +func (c *Ctx) String() string { + return fmt.Sprintf( + "#%016X - %s <-> %s - %s %s", + c.fasthttp.ID(), + c.fasthttp.LocalAddr(), + c.fasthttp.RemoteAddr(), + c.fasthttp.Request.Header.Method(), + c.fasthttp.URI().FullURI(), + ) } // Type sets the Content-Type HTTP header to the MIME type specified by the file extension. -func (ctx *Ctx) Type(extension string, charset ...string) *Ctx { +func (c *Ctx) Type(extension string, charset ...string) *Ctx { if len(charset) > 0 { - ctx.Fasthttp.Response.Header.SetContentType(utils.GetMIME(extension) + "; charset=" + charset[0]) + c.fasthttp.Response.Header.SetContentType(utils.GetMIME(extension) + "; charset=" + charset[0]) } else { - ctx.Fasthttp.Response.Header.SetContentType(utils.GetMIME(extension)) + c.fasthttp.Response.Header.SetContentType(utils.GetMIME(extension)) } - return ctx + return c } // Vary adds the given header field to the Vary response header. // This will append the header, if not already listed, otherwise leaves it listed in the current location. -func (ctx *Ctx) Vary(fields ...string) { - ctx.Append(HeaderVary, fields...) +func (c *Ctx) Vary(fields ...string) { + c.Append(HeaderVary, fields...) } -// Write appends any input to the HTTP body response, io.Reader is also supported as input. -func (ctx *Ctx) Write(bodies ...interface{}) { - for i := range bodies { - switch body := bodies[i].(type) { - case string: - ctx.Fasthttp.Response.AppendBodyString(body) - case []byte: - ctx.Fasthttp.Response.AppendBodyString(getString(body)) - case int: - ctx.Fasthttp.Response.AppendBodyString(strconv.Itoa(body)) - case bool: - ctx.Fasthttp.Response.AppendBodyString(strconv.FormatBool(body)) - case io.Reader: - ctx.Fasthttp.Response.SetBodyStream(body, -1) - ctx.Set(HeaderContentLength, strconv.Itoa(len(ctx.Fasthttp.Response.Body()))) - default: - ctx.Fasthttp.Response.AppendBodyString(fmt.Sprintf("%v", body)) - } - } +// Write writes p into response body. +func (c *Ctx) Write(p []byte) (n int, err error) { + c.fasthttp.Response.AppendBody(p) + return len(p), nil } // XHR returns a Boolean property, that is true, if the request's X-Requested-With header field is XMLHttpRequest, // indicating that the request was issued by a client library (such as jQuery). -func (ctx *Ctx) XHR() bool { - return strings.EqualFold(ctx.Get(HeaderXRequestedWith), "xmlhttprequest") +func (c *Ctx) XHR() bool { + return utils.EqualsFold(utils.GetBytes(c.Get(HeaderXRequestedWith)), []byte("xmlhttprequest")) } // prettifyPath ... -func (ctx *Ctx) prettifyPath() { +func (c *Ctx) prettifyPath() { // If UnescapePath enabled, we decode the path - if ctx.app.Settings.UnescapePath { - pathBytes := getBytes(ctx.path) - pathBytes = fasthttp.AppendUnquotedArg(pathBytes[:0], pathBytes) - ctx.path = getString(pathBytes) + if c.app.config.UnescapePath { + c.pathBuffer = fasthttp.AppendUnquotedArg(c.pathBuffer[:0], c.pathBuffer) } // If CaseSensitive is disabled, we lowercase the original path - if !ctx.app.Settings.CaseSensitive { - // We are making a copy here to keep access to the original path - ctx.path = utils.ToLower(ctx.path) + if !c.app.config.CaseSensitive { + c.pathBuffer = utils.ToLowerBytes(c.pathBuffer) } // If StrictRouting is disabled, we strip all trailing slashes - if !ctx.app.Settings.StrictRouting && len(ctx.path) > 1 && ctx.path[len(ctx.path)-1] == '/' { - ctx.path = utils.TrimRight(ctx.path, '/') + if !c.app.config.StrictRouting && len(c.pathBuffer) > 1 && c.pathBuffer[len(c.pathBuffer)-1] == '/' { + c.pathBuffer = utils.TrimRightBytes(c.pathBuffer, '/') } + c.path = getString(c.pathBuffer) - ctx.treePath = ctx.treePath[0:0] - if len(ctx.path) >= 3 { - ctx.treePath = ctx.path[:3] + c.treePath = c.treePath[0:0] + if len(c.path) >= 3 { + c.treePath = c.path[:3] } } diff --git a/ctx_test.go b/ctx_test.go index 3715303d..867bac8d 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -24,23 +24,22 @@ import ( "text/template" "time" - "github.com/valyala/bytebufferpool" - - utils "github.com/gofiber/utils" - fasthttp "github.com/valyala/fasthttp" + "github.com/gofiber/fiber/v2/utils" + "github.com/gofiber/fiber/v2/utils/bytebufferpool" + "github.com/valyala/fasthttp" ) // go test -run Test_Ctx_Accepts func Test_Ctx_Accepts(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.Fasthttp.Request.Header.Set(HeaderAccept, "text/html,application/xhtml+xml,application/xml;q=0.9") - utils.AssertEqual(t, "", ctx.Accepts("")) - utils.AssertEqual(t, "", ctx.Accepts()) - utils.AssertEqual(t, ".xml", ctx.Accepts(".xml")) - utils.AssertEqual(t, "", ctx.Accepts(".john")) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderAccept, "text/html,application/xhtml+xml,application/xml;q=0.9") + utils.AssertEqual(t, "", c.Accepts("")) + utils.AssertEqual(t, "", c.Accepts()) + utils.AssertEqual(t, ".xml", c.Accepts(".xml")) + utils.AssertEqual(t, "", c.Accepts(".john")) } // go test -v -run=^$ -bench=Benchmark_Ctx_Accepts -benchmem -count=4 @@ -48,7 +47,7 @@ func Benchmark_Ctx_Accepts(b *testing.B) { app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) - c.Fasthttp.Request.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9") + c.Request().Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9") var res string b.ReportAllocs() b.ResetTimer() @@ -62,33 +61,33 @@ func Benchmark_Ctx_Accepts(b *testing.B) { func Test_Ctx_Accepts_EmptyAccept(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - utils.AssertEqual(t, ".forwarded", ctx.Accepts(".forwarded")) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + utils.AssertEqual(t, ".forwarded", c.Accepts(".forwarded")) } // go test -run Test_Ctx_Accepts_Wildcard func Test_Ctx_Accepts_Wildcard(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.Fasthttp.Request.Header.Set(HeaderAccept, "*/*;q=0.9") - utils.AssertEqual(t, "html", ctx.Accepts("html")) - utils.AssertEqual(t, "foo", ctx.Accepts("foo")) - utils.AssertEqual(t, ".bar", ctx.Accepts(".bar")) - ctx.Fasthttp.Request.Header.Set(HeaderAccept, "text/html,application/*;q=0.9") - utils.AssertEqual(t, "xml", ctx.Accepts("xml")) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderAccept, "*/*;q=0.9") + utils.AssertEqual(t, "html", c.Accepts("html")) + utils.AssertEqual(t, "foo", c.Accepts("foo")) + utils.AssertEqual(t, ".bar", c.Accepts(".bar")) + c.Request().Header.Set(HeaderAccept, "text/html,application/*;q=0.9") + utils.AssertEqual(t, "xml", c.Accepts("xml")) } // go test -run Test_Ctx_AcceptsCharsets func Test_Ctx_AcceptsCharsets(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.Fasthttp.Request.Header.Set(HeaderAcceptCharset, "utf-8, iso-8859-1;q=0.5") - utils.AssertEqual(t, "utf-8", ctx.AcceptsCharsets("utf-8")) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderAcceptCharset, "utf-8, iso-8859-1;q=0.5") + utils.AssertEqual(t, "utf-8", c.AcceptsCharsets("utf-8")) } // go test -v -run=^$ -bench=Benchmark_Ctx_AcceptsCharsets -benchmem -count=4 @@ -96,7 +95,7 @@ func Benchmark_Ctx_AcceptsCharsets(b *testing.B) { app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) - c.Fasthttp.Request.Header.Set("Accept-Charset", "utf-8, iso-8859-1;q=0.5") + c.Request().Header.Set("Accept-Charset", "utf-8, iso-8859-1;q=0.5") var res string b.ReportAllocs() b.ResetTimer() @@ -110,11 +109,11 @@ func Benchmark_Ctx_AcceptsCharsets(b *testing.B) { func Test_Ctx_AcceptsEncodings(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.Fasthttp.Request.Header.Set(HeaderAcceptEncoding, "deflate, gzip;q=1.0, *;q=0.5") - utils.AssertEqual(t, "gzip", ctx.AcceptsEncodings("gzip")) - utils.AssertEqual(t, "abc", ctx.AcceptsEncodings("abc")) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderAcceptEncoding, "deflate, gzip;q=1.0, *;q=0.5") + utils.AssertEqual(t, "gzip", c.AcceptsEncodings("gzip")) + utils.AssertEqual(t, "abc", c.AcceptsEncodings("abc")) } // go test -v -run=^$ -bench=Benchmark_Ctx_AcceptsEncodings -benchmem -count=4 @@ -122,7 +121,7 @@ func Benchmark_Ctx_AcceptsEncodings(b *testing.B) { app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) - c.Fasthttp.Request.Header.Set(HeaderAcceptEncoding, "deflate, gzip;q=1.0, *;q=0.5") + c.Request().Header.Set(HeaderAcceptEncoding, "deflate, gzip;q=1.0, *;q=0.5") var res string b.ReportAllocs() b.ResetTimer() @@ -136,10 +135,10 @@ func Benchmark_Ctx_AcceptsEncodings(b *testing.B) { func Test_Ctx_AcceptsLanguages(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.Fasthttp.Request.Header.Set(HeaderAcceptLanguage, "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5") - utils.AssertEqual(t, "fr", ctx.AcceptsLanguages("fr")) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderAcceptLanguage, "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5") + utils.AssertEqual(t, "fr", c.AcceptsLanguages("fr")) } // go test -v -run=^$ -bench=Benchmark_Ctx_AcceptsLanguages -benchmem -count=4 @@ -147,7 +146,7 @@ func Benchmark_Ctx_AcceptsLanguages(b *testing.B) { app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) - c.Fasthttp.Request.Header.Set(HeaderAcceptLanguage, "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5") + c.Request().Header.Set(HeaderAcceptLanguage, "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5") var res string b.ReportAllocs() b.ResetTimer() @@ -161,46 +160,46 @@ func Benchmark_Ctx_AcceptsLanguages(b *testing.B) { func Test_Ctx_App(t *testing.T) { t.Parallel() app := New() - app.Settings.BodyLimit = 1000 - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - utils.AssertEqual(t, 1000, ctx.App().Settings.BodyLimit) + app.config.BodyLimit = 1000 + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + utils.AssertEqual(t, 1000, c.App().config.BodyLimit) } // go test -run Test_Ctx_Append func Test_Ctx_Append(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.Append("X-Test", "Hello") - ctx.Append("X-Test", "World") - ctx.Append("X-Test", "Hello", "World") + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Append("X-Test", "Hello") + c.Append("X-Test", "World") + c.Append("X-Test", "Hello", "World") // similar value in the middle - ctx.Append("X2-Test", "World") - ctx.Append("X2-Test", "XHello") - ctx.Append("X2-Test", "Hello", "World") + c.Append("X2-Test", "World") + c.Append("X2-Test", "XHello") + c.Append("X2-Test", "Hello", "World") // similar value at the start - ctx.Append("X3-Test", "XHello") - ctx.Append("X3-Test", "World") - ctx.Append("X3-Test", "Hello", "World") + c.Append("X3-Test", "XHello") + c.Append("X3-Test", "World") + c.Append("X3-Test", "Hello", "World") // try it with multiple similar values - ctx.Append("X4-Test", "XHello") - ctx.Append("X4-Test", "Hello") - ctx.Append("X4-Test", "HelloZ") - ctx.Append("X4-Test", "YHello") - ctx.Append("X4-Test", "Hello") - ctx.Append("X4-Test", "YHello") - ctx.Append("X4-Test", "HelloZ") - ctx.Append("X4-Test", "XHello") + c.Append("X4-Test", "XHello") + c.Append("X4-Test", "Hello") + c.Append("X4-Test", "HelloZ") + c.Append("X4-Test", "YHello") + c.Append("X4-Test", "Hello") + c.Append("X4-Test", "YHello") + c.Append("X4-Test", "HelloZ") + c.Append("X4-Test", "XHello") // without append value - ctx.Append("X-Custom-Header") + c.Append("X-Custom-Header") - utils.AssertEqual(t, "Hello, World", string(ctx.Fasthttp.Response.Header.Peek("X-Test"))) - utils.AssertEqual(t, "World, XHello, Hello", string(ctx.Fasthttp.Response.Header.Peek("X2-Test"))) - utils.AssertEqual(t, "XHello, World, Hello", string(ctx.Fasthttp.Response.Header.Peek("X3-Test"))) - utils.AssertEqual(t, "XHello, Hello, HelloZ, YHello", string(ctx.Fasthttp.Response.Header.Peek("X4-Test"))) - utils.AssertEqual(t, "", string(ctx.Fasthttp.Response.Header.Peek("x-custom-header"))) + utils.AssertEqual(t, "Hello, World", string(c.Response().Header.Peek("X-Test"))) + utils.AssertEqual(t, "World, XHello, Hello", string(c.Response().Header.Peek("X2-Test"))) + utils.AssertEqual(t, "XHello, World, Hello", string(c.Response().Header.Peek("X3-Test"))) + utils.AssertEqual(t, "XHello, Hello, HelloZ, YHello", string(c.Response().Header.Peek("X4-Test"))) + utils.AssertEqual(t, "", string(c.Response().Header.Peek("x-custom-header"))) } // go test -v -run=^$ -bench=Benchmark_Ctx_Append -benchmem -count=4 @@ -215,25 +214,25 @@ func Benchmark_Ctx_Append(b *testing.B) { c.Append("X-Custom-Header", "World") c.Append("X-Custom-Header", "Hello") } - utils.AssertEqual(b, "Hello, World", getString(c.Fasthttp.Response.Header.Peek("X-Custom-Header"))) + utils.AssertEqual(b, "Hello, World", getString(c.Response().Header.Peek("X-Custom-Header"))) } // go test -run Test_Ctx_Attachment func Test_Ctx_Attachment(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) // empty - ctx.Attachment() - utils.AssertEqual(t, `attachment`, string(ctx.Fasthttp.Response.Header.Peek(HeaderContentDisposition))) + c.Attachment() + utils.AssertEqual(t, `attachment`, string(c.Response().Header.Peek(HeaderContentDisposition))) // real filename - ctx.Attachment("./static/img/logo.png") - utils.AssertEqual(t, `attachment; filename="logo.png"`, string(ctx.Fasthttp.Response.Header.Peek(HeaderContentDisposition))) - utils.AssertEqual(t, "image/png", string(ctx.Fasthttp.Response.Header.Peek(HeaderContentType))) + c.Attachment("./static/img/logo.png") + utils.AssertEqual(t, `attachment; filename="logo.png"`, string(c.Response().Header.Peek(HeaderContentDisposition))) + utils.AssertEqual(t, "image/png", string(c.Response().Header.Peek(HeaderContentType))) // check quoting - ctx.Attachment("another document.pdf\"\r\nBla: \"fasel") - utils.AssertEqual(t, `attachment; filename="another+document.pdf%22%0D%0ABla%3A+%22fasel"`, string(ctx.Fasthttp.Response.Header.Peek(HeaderContentDisposition))) + c.Attachment("another document.pdf\"\r\nBla: \"fasel") + utils.AssertEqual(t, `attachment; filename="another+document.pdf%22%0D%0ABla%3A+%22fasel"`, string(c.Response().Header.Peek(HeaderContentDisposition))) } // go test -v -run=^$ -bench=Benchmark_Ctx_Attachment -benchmem -count=4 @@ -247,17 +246,17 @@ func Benchmark_Ctx_Attachment(b *testing.B) { // example with quote params c.Attachment("another document.pdf\"\r\nBla: \"fasel") } - utils.AssertEqual(b, `attachment; filename="another+document.pdf%22%0D%0ABla%3A+%22fasel"`, string(c.Fasthttp.Response.Header.Peek(HeaderContentDisposition))) + utils.AssertEqual(b, `attachment; filename="another+document.pdf%22%0D%0ABla%3A+%22fasel"`, string(c.Response().Header.Peek(HeaderContentDisposition))) } // go test -run Test_Ctx_BaseURL func Test_Ctx_BaseURL(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.Fasthttp.Request.SetRequestURI("http://google.com/test") - utils.AssertEqual(t, "http://google.com", ctx.BaseURL()) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().SetRequestURI("http://google.com/test") + utils.AssertEqual(t, "http://google.com", c.BaseURL()) } // go test -v -run=^$ -bench=Benchmark_Ctx_Append -benchmem -count=4 @@ -265,8 +264,8 @@ func Benchmark_Ctx_BaseURL(b *testing.B) { app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) - c.Fasthttp.Request.SetHost("google.com:1337") - c.Fasthttp.Request.URI().SetPath("/haha/oke/lol") + c.Request().SetHost("google.com:1337") + c.Request().URI().SetPath("/haha/oke/lol") var res string b.ReportAllocs() b.ResetTimer() @@ -280,29 +279,29 @@ func Benchmark_Ctx_BaseURL(b *testing.B) { func Test_Ctx_Body(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.Fasthttp.Request.SetBody([]byte("john=doe")) - utils.AssertEqual(t, "john=doe", ctx.Body()) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().SetBody([]byte("john=doe")) + utils.AssertEqual(t, []byte("john=doe"), c.Body()) } // go test -run Test_Ctx_BodyParser func Test_Ctx_BodyParser(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) type Demo struct { Name string `json:"name" xml:"name" form:"name" query:"name"` } testDecodeParser := func(contentType, body string) { - ctx.Fasthttp.Request.Header.SetContentType(contentType) - ctx.Fasthttp.Request.SetBody([]byte(body)) - ctx.Fasthttp.Request.Header.SetContentLength(len(body)) + c.Request().Header.SetContentType(contentType) + c.Request().SetBody([]byte(body)) + c.Request().Header.SetContentLength(len(body)) d := new(Demo) - utils.AssertEqual(t, nil, ctx.BodyParser(d)) + utils.AssertEqual(t, nil, c.BodyParser(d)) utils.AssertEqual(t, "john", d.Name) } @@ -313,10 +312,10 @@ func Test_Ctx_BodyParser(t *testing.T) { testDecodeParser(MIMEMultipartForm+`;boundary="b"`, "--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njohn\r\n--b--") testDecodeParserError := func(contentType, body string) { - ctx.Fasthttp.Request.Header.SetContentType(contentType) - ctx.Fasthttp.Request.SetBody([]byte(body)) - ctx.Fasthttp.Request.Header.SetContentLength(len(body)) - utils.AssertEqual(t, false, ctx.BodyParser(nil) == nil) + c.Request().Header.SetContentType(contentType) + c.Request().SetBody([]byte(body)) + c.Request().Header.SetContentLength(len(body)) + utils.AssertEqual(t, false, c.BodyParser(nil) == nil) } testDecodeParserError("invalid-content-type", "") @@ -332,9 +331,9 @@ func Benchmark_Ctx_BodyParser_JSON(b *testing.B) { Name string `json:"name"` } body := []byte(`{"name":"john"}`) - c.Fasthttp.Request.SetBody(body) - c.Fasthttp.Request.Header.SetContentType(MIMEApplicationJSON) - c.Fasthttp.Request.Header.SetContentLength(len(body)) + c.Request().SetBody(body) + c.Request().Header.SetContentType(MIMEApplicationJSON) + c.Request().Header.SetContentLength(len(body)) d := new(Demo) b.ReportAllocs() @@ -356,9 +355,9 @@ func Benchmark_Ctx_BodyParser_XML(b *testing.B) { Name string `xml:"name"` } body := []byte("john") - c.Fasthttp.Request.SetBody(body) - c.Fasthttp.Request.Header.SetContentType(MIMEApplicationXML) - c.Fasthttp.Request.Header.SetContentLength(len(body)) + c.Request().SetBody(body) + c.Request().Header.SetContentType(MIMEApplicationXML) + c.Request().Header.SetContentLength(len(body)) d := new(Demo) b.ReportAllocs() @@ -380,9 +379,9 @@ func Benchmark_Ctx_BodyParser_Form(b *testing.B) { Name string `form:"name"` } body := []byte("name=john") - c.Fasthttp.Request.SetBody(body) - c.Fasthttp.Request.Header.SetContentType(MIMEApplicationForm) - c.Fasthttp.Request.Header.SetContentLength(len(body)) + c.Request().SetBody(body) + c.Request().Header.SetContentType(MIMEApplicationForm) + c.Request().Header.SetContentLength(len(body)) d := new(Demo) b.ReportAllocs() @@ -405,9 +404,9 @@ func Benchmark_Ctx_BodyParser_MultipartForm(b *testing.B) { } body := []byte("--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njohn\r\n--b--") - c.Fasthttp.Request.SetBody(body) - c.Fasthttp.Request.Header.SetContentType(MIMEMultipartForm + `;boundary="b"`) - c.Fasthttp.Request.Header.SetContentLength(len(body)) + c.Request().SetBody(body) + c.Request().Header.SetContentType(MIMEMultipartForm + `;boundary="b"`) + c.Request().Header.SetContentLength(len(body)) d := new(Demo) b.ReportAllocs() @@ -424,32 +423,32 @@ func Benchmark_Ctx_BodyParser_MultipartForm(b *testing.B) { func Test_Ctx_Context(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) - utils.AssertEqual(t, "*fasthttp.RequestCtx", fmt.Sprintf("%T", ctx.Context())) + utils.AssertEqual(t, "*fasthttp.RequestCtx", fmt.Sprintf("%T", c.Context())) } // go test -run Test_Ctx_Cookie func Test_Ctx_Cookie(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) expire := time.Now().Add(24 * time.Hour) var dst []byte dst = expire.In(time.UTC).AppendFormat(dst, time.RFC1123) httpdate := strings.Replace(string(dst), "UTC", "GMT", -1) - ctx.Cookie(&Cookie{ + c.Cookie(&Cookie{ Name: "username", Value: "john", Expires: expire, }) expect := "username=john; expires=" + httpdate + "; path=/; SameSite=Lax" - utils.AssertEqual(t, expect, string(ctx.Fasthttp.Response.Header.Peek(HeaderSetCookie))) + utils.AssertEqual(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) - ctx.Cookie(&Cookie{SameSite: "strict"}) - ctx.Cookie(&Cookie{SameSite: "none"}) + c.Cookie(&Cookie{SameSite: "strict"}) + c.Cookie(&Cookie{SameSite: "none"}) } // go test -v -run=^$ -bench=Benchmark_Ctx_Cookie -benchmem -count=4 @@ -465,50 +464,54 @@ func Benchmark_Ctx_Cookie(b *testing.B) { Value: "Doe", }) } - utils.AssertEqual(b, "John=Doe; path=/; SameSite=Lax", getString(c.Fasthttp.Response.Header.Peek("Set-Cookie"))) + utils.AssertEqual(b, "John=Doe; path=/; SameSite=Lax", getString(c.Response().Header.Peek("Set-Cookie"))) } // go test -run Test_Ctx_Cookies func Test_Ctx_Cookies(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.Fasthttp.Request.Header.Set("Cookie", "john=doe") - utils.AssertEqual(t, "doe", ctx.Cookies("john")) - utils.AssertEqual(t, "default", ctx.Cookies("unknown", "default")) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set("Cookie", "john=doe") + utils.AssertEqual(t, "doe", c.Cookies("john")) + utils.AssertEqual(t, "default", c.Cookies("unknown", "default")) } // go test -run Test_Ctx_Format func Test_Ctx_Format(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.Fasthttp.Request.Header.Set(HeaderAccept, MIMETextPlain) - ctx.Format([]byte("Hello, World!")) - utils.AssertEqual(t, "Hello, World!", string(ctx.Fasthttp.Response.Body())) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderAccept, MIMETextPlain) + c.Format([]byte("Hello, World!")) + utils.AssertEqual(t, "Hello, World!", string(c.Response().Body())) - ctx.Fasthttp.Request.Header.Set(HeaderAccept, MIMETextHTML) - ctx.Format("Hello, World!") - utils.AssertEqual(t, "

Hello, World!

", string(ctx.Fasthttp.Response.Body())) + c.Request().Header.Set(HeaderAccept, MIMETextHTML) + c.Format("Hello, World!") + utils.AssertEqual(t, "

Hello, World!

", string(c.Response().Body())) - ctx.Fasthttp.Request.Header.Set(HeaderAccept, MIMEApplicationJSON) - ctx.Format("Hello, World!") - utils.AssertEqual(t, `"Hello, World!"`, string(ctx.Fasthttp.Response.Body())) - ctx.Format(complex(1, 1)) - utils.AssertEqual(t, "(1+1i)", string(ctx.Fasthttp.Response.Body())) + c.Request().Header.Set(HeaderAccept, MIMEApplicationJSON) + c.Format("Hello, World!") + utils.AssertEqual(t, `"Hello, World!"`, string(c.Response().Body())) - ctx.Fasthttp.Request.Header.Set(HeaderAccept, MIMEApplicationXML) - ctx.Format("Hello, World!") - utils.AssertEqual(t, `Hello, World!`, string(ctx.Fasthttp.Response.Body())) - ctx.Format(Map{}) - utils.AssertEqual(t, "map[]", string(ctx.Fasthttp.Response.Body())) + c.Request().Header.Set(HeaderAccept, MIMETextPlain) + c.Format(complex(1, 1)) + utils.AssertEqual(t, "(1+1i)", string(c.Response().Body())) + + c.Request().Header.Set(HeaderAccept, MIMEApplicationXML) + c.Format("Hello, World!") + utils.AssertEqual(t, `Hello, World!`, string(c.Response().Body())) + + c.Request().Header.Set(HeaderAccept, MIMETextPlain) + c.Format(Map{}) + utils.AssertEqual(t, "map[]", string(c.Response().Body())) type broken string - ctx.Fasthttp.Request.Header.Set(HeaderAccept, "broken/accept") - ctx.Format(broken("Hello, World!")) - utils.AssertEqual(t, `Hello, World!`, string(ctx.Fasthttp.Response.Body())) + c.Request().Header.Set(HeaderAccept, "broken/accept") + c.Format(broken("Hello, World!")) + utils.AssertEqual(t, `Hello, World!`, string(c.Response().Body())) } // go test -v -run=^$ -bench=Benchmark_Ctx_Format -benchmem -count=4 @@ -516,13 +519,13 @@ func Benchmark_Ctx_Format(b *testing.B) { app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) - c.Fasthttp.Request.Header.Set("Accept", "text/plain") + c.Request().Header.Set("Accept", "text/plain") b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { c.Format("Hello, World!") } - utils.AssertEqual(b, `Hello, World!`, string(c.Fasthttp.Response.Body())) + utils.AssertEqual(b, `Hello, World!`, string(c.Response().Body())) } // go test -v -run=^$ -bench=Benchmark_Ctx_Format_HTML -benchmem -count=4 @@ -530,13 +533,13 @@ func Benchmark_Ctx_Format_HTML(b *testing.B) { app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) - c.Fasthttp.Request.Header.Set("Accept", "text/html") + c.Request().Header.Set("Accept", "text/html") b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { c.Format("Hello, World!") } - utils.AssertEqual(b, "

Hello, World!

", string(c.Fasthttp.Response.Body())) + utils.AssertEqual(b, "

Hello, World!

", string(c.Response().Body())) } // go test -v -run=^$ -bench=Benchmark_Ctx_Format_JSON -benchmem -count=4 @@ -544,13 +547,13 @@ func Benchmark_Ctx_Format_JSON(b *testing.B) { app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) - c.Fasthttp.Request.Header.Set("Accept", "application/json") + c.Request().Header.Set("Accept", "application/json") b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { c.Format("Hello, World!") } - utils.AssertEqual(b, `"Hello, World!"`, string(c.Fasthttp.Response.Body())) + utils.AssertEqual(b, `"Hello, World!"`, string(c.Response().Body())) } // go test -v -run=^$ -bench=Benchmark_Ctx_Format_XML -benchmem -count=4 @@ -558,13 +561,13 @@ func Benchmark_Ctx_Format_XML(b *testing.B) { app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) - c.Fasthttp.Request.Header.Set("Accept", "application/xml") + c.Request().Header.Set("Accept", "application/xml") b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { c.Format("Hello, World!") } - utils.AssertEqual(b, `Hello, World!`, string(c.Fasthttp.Response.Body())) + utils.AssertEqual(b, `Hello, World!`, string(c.Response().Body())) } // go test -run Test_Ctx_FormFile @@ -573,7 +576,7 @@ func Test_Ctx_FormFile(t *testing.T) { t.Parallel() app := New() - app.Post("/test", func(c *Ctx) { + app.Post("/test", func(c *Ctx) error { fh, err := c.FormFile("file") utils.AssertEqual(t, nil, err) utils.AssertEqual(t, "test", fh.Filename) @@ -587,6 +590,7 @@ func Test_Ctx_FormFile(t *testing.T) { f.Close() utils.AssertEqual(t, "hello world", b.String()) + return nil }) body := &bytes.Buffer{} @@ -614,8 +618,9 @@ func Test_Ctx_FormValue(t *testing.T) { t.Parallel() app := New() - app.Post("/test", func(c *Ctx) { + app.Post("/test", func(c *Ctx) error { utils.AssertEqual(t, "john", c.FormValue("name")) + return nil }) body := &bytes.Buffer{} @@ -636,17 +641,17 @@ func Test_Ctx_FormValue(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Ctx_Fresh_StaleEtag -benchmem -count=4 func Benchmark_Ctx_Fresh_StaleEtag(b *testing.B) { app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) for n := 0; n < b.N; n++ { - ctx.Fasthttp.Request.Header.Set(HeaderIfNoneMatch, "a, b, c, d") - ctx.Fasthttp.Request.Header.Set(HeaderCacheControl, "c") - ctx.Fresh() + c.Request().Header.Set(HeaderIfNoneMatch, "a, b, c, d") + c.Request().Header.Set(HeaderCacheControl, "c") + c.Fresh() - ctx.Fasthttp.Request.Header.Set(HeaderIfNoneMatch, "a, b, c, d") - ctx.Fasthttp.Request.Header.Set(HeaderCacheControl, "e") - ctx.Fresh() + c.Request().Header.Set(HeaderIfNoneMatch, "a, b, c, d") + c.Request().Header.Set(HeaderCacheControl, "e") + c.Fresh() } } @@ -654,58 +659,58 @@ func Benchmark_Ctx_Fresh_StaleEtag(b *testing.B) { func Test_Ctx_Fresh(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - utils.AssertEqual(t, false, ctx.Fresh()) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + utils.AssertEqual(t, false, c.Fresh()) - ctx.Fasthttp.Request.Header.Set(HeaderIfNoneMatch, "*") - ctx.Fasthttp.Request.Header.Set(HeaderCacheControl, "no-cache") - utils.AssertEqual(t, false, ctx.Fresh()) + c.Request().Header.Set(HeaderIfNoneMatch, "*") + c.Request().Header.Set(HeaderCacheControl, "no-cache") + utils.AssertEqual(t, false, c.Fresh()) - ctx.Fasthttp.Request.Header.Set(HeaderIfNoneMatch, "*") - ctx.Fasthttp.Request.Header.Set(HeaderCacheControl, ",no-cache,") - utils.AssertEqual(t, false, ctx.Fresh()) + c.Request().Header.Set(HeaderIfNoneMatch, "*") + c.Request().Header.Set(HeaderCacheControl, ",no-cache,") + utils.AssertEqual(t, false, c.Fresh()) - ctx.Fasthttp.Request.Header.Set(HeaderIfNoneMatch, "*") - ctx.Fasthttp.Request.Header.Set(HeaderCacheControl, "aa,no-cache,") - utils.AssertEqual(t, false, ctx.Fresh()) + c.Request().Header.Set(HeaderIfNoneMatch, "*") + c.Request().Header.Set(HeaderCacheControl, "aa,no-cache,") + utils.AssertEqual(t, false, c.Fresh()) - ctx.Fasthttp.Request.Header.Set(HeaderIfNoneMatch, "*") - ctx.Fasthttp.Request.Header.Set(HeaderCacheControl, ",no-cache,bb") - utils.AssertEqual(t, false, ctx.Fresh()) + c.Request().Header.Set(HeaderIfNoneMatch, "*") + c.Request().Header.Set(HeaderCacheControl, ",no-cache,bb") + utils.AssertEqual(t, false, c.Fresh()) - ctx.Fasthttp.Request.Header.Set(HeaderIfNoneMatch, "675af34563dc-tr34") - ctx.Fasthttp.Request.Header.Set(HeaderCacheControl, "public") - utils.AssertEqual(t, false, ctx.Fresh()) + c.Request().Header.Set(HeaderIfNoneMatch, "675af34563dc-tr34") + c.Request().Header.Set(HeaderCacheControl, "public") + utils.AssertEqual(t, false, c.Fresh()) - ctx.Fasthttp.Request.Header.Set(HeaderIfNoneMatch, "a, b") - ctx.Fasthttp.Response.Header.Set(HeaderETag, "c") - utils.AssertEqual(t, false, ctx.Fresh()) + c.Request().Header.Set(HeaderIfNoneMatch, "a, b") + c.Response().Header.Set(HeaderETag, "c") + utils.AssertEqual(t, false, c.Fresh()) - ctx.Fasthttp.Response.Header.Set(HeaderETag, "a") - utils.AssertEqual(t, true, ctx.Fresh()) + c.Response().Header.Set(HeaderETag, "a") + utils.AssertEqual(t, true, c.Fresh()) - ctx.Fasthttp.Request.Header.Set(HeaderIfModifiedSince, "xxWed, 21 Oct 2015 07:28:00 GMT") - ctx.Fasthttp.Response.Header.Set(HeaderLastModified, "xxWed, 21 Oct 2015 07:28:00 GMT") - utils.AssertEqual(t, false, ctx.Fresh()) + c.Request().Header.Set(HeaderIfModifiedSince, "xxWed, 21 Oct 2015 07:28:00 GMT") + c.Response().Header.Set(HeaderLastModified, "xxWed, 21 Oct 2015 07:28:00 GMT") + utils.AssertEqual(t, false, c.Fresh()) - ctx.Fasthttp.Response.Header.Set(HeaderLastModified, "Wed, 21 Oct 2015 07:28:00 GMT") - utils.AssertEqual(t, false, ctx.Fresh()) + c.Response().Header.Set(HeaderLastModified, "Wed, 21 Oct 2015 07:28:00 GMT") + utils.AssertEqual(t, false, c.Fresh()) - ctx.Fasthttp.Request.Header.Set(HeaderIfModifiedSince, "Wed, 21 Oct 2015 07:28:00 GMT") - utils.AssertEqual(t, false, ctx.Fresh()) + c.Request().Header.Set(HeaderIfModifiedSince, "Wed, 21 Oct 2015 07:28:00 GMT") + utils.AssertEqual(t, false, c.Fresh()) } // go test -v -run=^$ -bench=Benchmark_Ctx_Fresh_WithNoCache -benchmem -count=4 func Benchmark_Ctx_Fresh_WithNoCache(b *testing.B) { app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) - ctx.Fasthttp.Request.Header.Set(HeaderIfNoneMatch, "*") - ctx.Fasthttp.Request.Header.Set(HeaderCacheControl, "no-cache") + c.Request().Header.Set(HeaderIfNoneMatch, "*") + c.Request().Header.Set(HeaderCacheControl, "no-cache") for n := 0; n < b.N; n++ { - ctx.Fresh() + c.Fresh() } } @@ -713,45 +718,48 @@ func Benchmark_Ctx_Fresh_WithNoCache(b *testing.B) { func Test_Ctx_Get(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.Fasthttp.Request.Header.Set(HeaderAcceptCharset, "utf-8, iso-8859-1;q=0.5") - ctx.Fasthttp.Request.Header.Set(HeaderReferer, "Monster") - utils.AssertEqual(t, "utf-8, iso-8859-1;q=0.5", ctx.Get(HeaderAcceptCharset)) - utils.AssertEqual(t, "Monster", ctx.Get(HeaderReferer)) - utils.AssertEqual(t, "default", ctx.Get("unknown", "default")) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderAcceptCharset, "utf-8, iso-8859-1;q=0.5") + c.Request().Header.Set(HeaderReferer, "Monster") + utils.AssertEqual(t, "utf-8, iso-8859-1;q=0.5", c.Get(HeaderAcceptCharset)) + utils.AssertEqual(t, "Monster", c.Get(HeaderReferer)) + utils.AssertEqual(t, "default", c.Get("unknown", "default")) } // go test -run Test_Ctx_Hostname func Test_Ctx_Hostname(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.Fasthttp.Request.SetRequestURI("http://google.com/test") - utils.AssertEqual(t, "google.com", ctx.Hostname()) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().SetRequestURI("http://google.com/test") + utils.AssertEqual(t, "google.com", c.Hostname()) } // go test -run Test_Ctx_IP func Test_Ctx_IP(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - utils.AssertEqual(t, "0.0.0.0", ctx.IP()) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + utils.AssertEqual(t, "0.0.0.0", c.IP()) } -// go test -run Test_Ctx_IPs +// go test -run Test_Ctx_IPs -parallel func Test_Ctx_IPs(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.Fasthttp.Request.Header.Set(HeaderXForwardedFor, "127.0.0.1, 127.0.0.1, 127.0.0.1") - utils.AssertEqual(t, []string{"127.0.0.1", "127.0.0.1", "127.0.0.1"}, ctx.IPs()) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderXForwardedFor, "127.0.0.1, 127.0.0.2, 127.0.0.3") + utils.AssertEqual(t, []string{"127.0.0.1", "127.0.0.2", "127.0.0.3"}, c.IPs()) - ctx.Fasthttp.Request.Header.Set(HeaderXForwardedFor, "") - utils.AssertEqual(t, 0, len(ctx.IPs())) + c.Request().Header.Set(HeaderXForwardedFor, "127.0.0.1,127.0.0.2 ,127.0.0.3") + utils.AssertEqual(t, []string{"127.0.0.1", "127.0.0.2", "127.0.0.3"}, c.IPs()) + + c.Request().Header.Set(HeaderXForwardedFor, "") + utils.AssertEqual(t, 0, len(c.IPs())) } // go test -v -run=^$ -bench=Benchmark_Ctx_IPs -benchmem -count=4 @@ -759,7 +767,7 @@ func Benchmark_Ctx_IPs(b *testing.B) { app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) - c.Fasthttp.Request.Header.Set(HeaderXForwardedFor, "127.0.0.1, 127.0.0.1, 127.0.0.1") + c.Request().Header.Set(HeaderXForwardedFor, "127.0.0.1, 127.0.0.1, 127.0.0.1") var res []string b.ReportAllocs() b.ResetTimer() @@ -773,35 +781,35 @@ func Benchmark_Ctx_IPs(b *testing.B) { func Test_Ctx_Is(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.Fasthttp.Request.Header.Set(HeaderContentType, MIMETextHTML+"; boundary=something") - utils.AssertEqual(t, true, ctx.Is(".html")) - utils.AssertEqual(t, true, ctx.Is("html")) - utils.AssertEqual(t, false, ctx.Is("json")) - utils.AssertEqual(t, false, ctx.Is(".json")) - utils.AssertEqual(t, false, ctx.Is("")) - utils.AssertEqual(t, false, ctx.Is(".foooo")) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderContentType, MIMETextHTML+"; boundary=something") + utils.AssertEqual(t, true, c.Is(".html")) + utils.AssertEqual(t, true, c.Is("html")) + utils.AssertEqual(t, false, c.Is("json")) + utils.AssertEqual(t, false, c.Is(".json")) + utils.AssertEqual(t, false, c.Is("")) + utils.AssertEqual(t, false, c.Is(".foooo")) - ctx.Fasthttp.Request.Header.Set(HeaderContentType, MIMEApplicationJSONCharsetUTF8) - utils.AssertEqual(t, false, ctx.Is("html")) - utils.AssertEqual(t, true, ctx.Is("json")) - utils.AssertEqual(t, true, ctx.Is(".json")) + c.Request().Header.Set(HeaderContentType, MIMEApplicationJSONCharsetUTF8) + utils.AssertEqual(t, false, c.Is("html")) + utils.AssertEqual(t, true, c.Is("json")) + utils.AssertEqual(t, true, c.Is(".json")) - ctx.Fasthttp.Request.Header.Set(HeaderContentType, " application/json;charset=UTF-8") - utils.AssertEqual(t, false, ctx.Is("html")) - utils.AssertEqual(t, true, ctx.Is("json")) - utils.AssertEqual(t, true, ctx.Is(".json")) + c.Request().Header.Set(HeaderContentType, " application/json;charset=UTF-8") + utils.AssertEqual(t, false, c.Is("html")) + utils.AssertEqual(t, true, c.Is("json")) + utils.AssertEqual(t, true, c.Is(".json")) - ctx.Fasthttp.Request.Header.Set(HeaderContentType, MIMEApplicationXMLCharsetUTF8) - utils.AssertEqual(t, false, ctx.Is("html")) - utils.AssertEqual(t, true, ctx.Is("xml")) - utils.AssertEqual(t, true, ctx.Is(".xml")) + c.Request().Header.Set(HeaderContentType, MIMEApplicationXMLCharsetUTF8) + utils.AssertEqual(t, false, c.Is("html")) + utils.AssertEqual(t, true, c.Is("xml")) + utils.AssertEqual(t, true, c.Is(".xml")) - ctx.Fasthttp.Request.Header.Set(HeaderContentType, MIMETextPlain) - utils.AssertEqual(t, false, ctx.Is("html")) - utils.AssertEqual(t, true, ctx.Is("txt")) - utils.AssertEqual(t, true, ctx.Is(".txt")) + c.Request().Header.Set(HeaderContentType, MIMETextPlain) + utils.AssertEqual(t, false, c.Is("html")) + utils.AssertEqual(t, true, c.Is("txt")) + utils.AssertEqual(t, true, c.Is(".txt")) } // go test -v -run=^$ -bench=Benchmark_Ctx_Is -benchmem -count=4 @@ -809,7 +817,7 @@ func Benchmark_Ctx_Is(b *testing.B) { app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) - c.Fasthttp.Request.Header.Set(HeaderContentType, MIMEApplicationJSON) + c.Request().Header.Set(HeaderContentType, MIMEApplicationJSON) var res bool b.ReportAllocs() b.ResetTimer() @@ -823,12 +831,13 @@ func Benchmark_Ctx_Is(b *testing.B) { // go test -run Test_Ctx_Locals func Test_Ctx_Locals(t *testing.T) { app := New() - app.Use(func(c *Ctx) { + app.Use(func(c *Ctx) error { c.Locals("john", "doe") - c.Next() + return c.Next() }) - app.Get("/test", func(c *Ctx) { + app.Get("/test", func(c *Ctx) error { utils.AssertEqual(t, "doe", c.Locals("john")) + return nil }) resp, err := app.Test(httptest.NewRequest(MethodGet, "/test", nil)) utils.AssertEqual(t, nil, err, "app.Test(req)") @@ -841,21 +850,23 @@ func Test_Ctx_Method(t *testing.T) { fctx := &fasthttp.RequestCtx{} fctx.Request.Header.SetMethod(MethodGet) app := New() - ctx := app.AcquireCtx(fctx) - defer app.ReleaseCtx(ctx) - utils.AssertEqual(t, MethodGet, ctx.Method()) - ctx.Method(MethodPost) - utils.AssertEqual(t, MethodPost, ctx.Method()) + c := app.AcquireCtx(fctx) + defer app.ReleaseCtx(c) + utils.AssertEqual(t, MethodGet, c.Method()) + c.Method(MethodPost) + utils.AssertEqual(t, MethodPost, c.Method()) - ctx.Method("MethodInvalid") - utils.AssertEqual(t, MethodPost, ctx.Method()) + c.Method("MethodInvalid") + utils.AssertEqual(t, MethodPost, c.Method()) } // go test -run Test_Ctx_InvalidMethod func Test_Ctx_InvalidMethod(t *testing.T) { t.Parallel() app := New() - app.Get("/", func(c *Ctx) {}) + app.Get("/", func(c *Ctx) error { + return nil + }) fctx := &fasthttp.RequestCtx{} fctx.Request.Header.SetMethod("InvalidMethod") @@ -872,10 +883,11 @@ func Test_Ctx_MultipartForm(t *testing.T) { t.Parallel() app := New() - app.Post("/test", func(c *Ctx) { + app.Post("/test", func(c *Ctx) error { result, err := c.MultipartForm() utils.AssertEqual(t, nil, err) utils.AssertEqual(t, "john", result.Value["name"][0]) + return nil }) body := &bytes.Buffer{} @@ -897,29 +909,33 @@ func Test_Ctx_MultipartForm(t *testing.T) { func Test_Ctx_OriginalURL(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.Fasthttp.Request.Header.SetRequestURI("http://google.com/test?search=demo") - utils.AssertEqual(t, "http://google.com/test?search=demo", ctx.OriginalURL()) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.SetRequestURI("http://google.com/test?search=demo") + utils.AssertEqual(t, "http://google.com/test?search=demo", c.OriginalURL()) } // go test -race -run Test_Ctx_Params func Test_Ctx_Params(t *testing.T) { t.Parallel() app := New() - app.Get("/test/:user", func(c *Ctx) { + app.Get("/test/:user", func(c *Ctx) error { utils.AssertEqual(t, "john", c.Params("user")) + return nil }) - app.Get("/test2/*", func(c *Ctx) { + app.Get("/test2/*", func(c *Ctx) error { utils.AssertEqual(t, "im/a/cookie", c.Params("*")) + return nil }) - app.Get("/test3/*/blafasel/*", func(c *Ctx) { + app.Get("/test3/*/blafasel/*", func(c *Ctx) error { utils.AssertEqual(t, "1111", c.Params("*1")) utils.AssertEqual(t, "2222", c.Params("*2")) utils.AssertEqual(t, "1111", c.Params("*")) + return nil }) - app.Get("/test4/:optional?", func(c *Ctx) { + app.Get("/test4/:optional?", func(c *Ctx) error { utils.AssertEqual(t, "", c.Params("optional")) + return nil }) resp, err := app.Test(httptest.NewRequest(MethodGet, "/test/john", nil)) utils.AssertEqual(t, nil, err, "app.Test(req)") @@ -948,7 +964,7 @@ func Benchmark_Ctx_Params(b *testing.B) { "param1", "param2", "param3", "param4", }, } - c.values = []string{ + c.values = [maxParams]string{ "john", "doe", "is", "awesome", } var res string @@ -967,11 +983,12 @@ func Benchmark_Ctx_Params(b *testing.B) { func Test_Ctx_Path(t *testing.T) { t.Parallel() app := New() - app.Get("/test/:user", func(c *Ctx) { + app.Get("/test/:user", func(c *Ctx) error { utils.AssertEqual(t, "/test/john", c.Path()) // not strict && case insensitive utils.AssertEqual(t, "/ABC/", c.Path("/ABC/")) utils.AssertEqual(t, "/test/john/", c.Path("/test/john/")) + return nil }) resp, err := app.Test(httptest.NewRequest(MethodGet, "/test/john", nil)) utils.AssertEqual(t, nil, err, "app.Test(req)") @@ -985,25 +1002,25 @@ func Test_Ctx_Protocol(t *testing.T) { freq := &fasthttp.RequestCtx{} freq.Request.Header.Set("X-Forwarded", "invalid") - ctx := app.AcquireCtx(freq) - defer app.ReleaseCtx(ctx) - ctx.Fasthttp.Request.Header.Set(HeaderXForwardedProto, "https") - utils.AssertEqual(t, "https", ctx.Protocol()) - ctx.Fasthttp.Request.Header.Reset() + c := app.AcquireCtx(freq) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderXForwardedProto, "https") + utils.AssertEqual(t, "https", c.Protocol()) + c.Request().Header.Reset() - ctx.Fasthttp.Request.Header.Set(HeaderXForwardedProtocol, "https") - utils.AssertEqual(t, "https", ctx.Protocol()) - ctx.Fasthttp.Request.Header.Reset() + c.Request().Header.Set(HeaderXForwardedProtocol, "https") + utils.AssertEqual(t, "https", c.Protocol()) + c.Request().Header.Reset() - ctx.Fasthttp.Request.Header.Set(HeaderXForwardedSsl, "on") - utils.AssertEqual(t, "https", ctx.Protocol()) - ctx.Fasthttp.Request.Header.Reset() + c.Request().Header.Set(HeaderXForwardedSsl, "on") + utils.AssertEqual(t, "https", c.Protocol()) + c.Request().Header.Reset() - ctx.Fasthttp.Request.Header.Set(HeaderXUrlScheme, "https") - utils.AssertEqual(t, "https", ctx.Protocol()) - ctx.Fasthttp.Request.Header.Reset() + c.Request().Header.Set(HeaderXUrlScheme, "https") + utils.AssertEqual(t, "https", c.Protocol()) + c.Request().Header.Reset() - utils.AssertEqual(t, "http", ctx.Protocol()) + utils.AssertEqual(t, "http", c.Protocol()) } // go test -v -run=^$ -bench=Benchmark_Ctx_Protocol -benchmem -count=4 @@ -1024,44 +1041,44 @@ func Benchmark_Ctx_Protocol(b *testing.B) { func Test_Ctx_Query(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.Fasthttp.Request.URI().SetQueryString("search=john&age=20") - utils.AssertEqual(t, "john", ctx.Query("search")) - utils.AssertEqual(t, "20", ctx.Query("age")) - utils.AssertEqual(t, "default", ctx.Query("unknown", "default")) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().URI().SetQueryString("search=john&age=20") + utils.AssertEqual(t, "john", c.Query("search")) + utils.AssertEqual(t, "20", c.Query("age")) + utils.AssertEqual(t, "default", c.Query("unknown", "default")) } // go test -run Test_Ctx_Range func Test_Ctx_Range(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) var ( result Range err error ) - result, err = ctx.Range(1000) + result, err = c.Range(1000) utils.AssertEqual(t, true, err != nil) - ctx.Fasthttp.Request.Header.Set(HeaderRange, "bytes=500") - result, err = ctx.Range(1000) + c.Request().Header.Set(HeaderRange, "bytes=500") + result, err = c.Range(1000) utils.AssertEqual(t, true, err != nil) - ctx.Fasthttp.Request.Header.Set(HeaderRange, "bytes=500=") - result, err = ctx.Range(1000) + c.Request().Header.Set(HeaderRange, "bytes=500=") + result, err = c.Range(1000) utils.AssertEqual(t, true, err != nil) - ctx.Fasthttp.Request.Header.Set(HeaderRange, "bytes=500-300") - result, err = ctx.Range(1000) + c.Request().Header.Set(HeaderRange, "bytes=500-300") + result, err = c.Range(1000) utils.AssertEqual(t, true, err != nil) testRange := func(header string, start, end int) { - ctx.Fasthttp.Request.Header.Set(HeaderRange, header) - result, err = ctx.Range(1000) + c.Request().Header.Set(HeaderRange, header) + result, err = c.Range(1000) utils.AssertEqual(t, nil, err) utils.AssertEqual(t, "bytes", result.Type) utils.AssertEqual(t, start, result.Ranges[0].Start) @@ -1078,27 +1095,29 @@ func Test_Ctx_Range(t *testing.T) { func Test_Ctx_Route(t *testing.T) { t.Parallel() app := New() - app.Get("/test", func(c *Ctx) { + app.Get("/test", func(c *Ctx) error { utils.AssertEqual(t, "/test", c.Route().Path) + return nil }) resp, err := app.Test(httptest.NewRequest(MethodGet, "/test", nil)) utils.AssertEqual(t, nil, err, "app.Test(req)") utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) - utils.AssertEqual(t, "/", ctx.Route().Path) - utils.AssertEqual(t, MethodGet, ctx.Route().Method) - utils.AssertEqual(t, 0, len(ctx.Route().Handlers)) + utils.AssertEqual(t, "/", c.Route().Path) + utils.AssertEqual(t, MethodGet, c.Route().Method) + utils.AssertEqual(t, 0, len(c.Route().Handlers)) } // go test -run Test_Ctx_RouteNormalized func Test_Ctx_RouteNormalized(t *testing.T) { t.Parallel() app := New() - app.Get("/test", func(c *Ctx) { + app.Get("/test", func(c *Ctx) error { utils.AssertEqual(t, "/test", c.Route().Path) + return nil }) resp, err := app.Test(httptest.NewRequest(MethodGet, "//test", nil)) utils.AssertEqual(t, nil, err, "app.Test(req)") @@ -1111,7 +1130,7 @@ func Test_Ctx_SaveFile(t *testing.T) { t.Parallel() app := New() - app.Post("/test", func(c *Ctx) { + app.Post("/test", func(c *Ctx) error { fh, err := c.FormFile("file") utils.AssertEqual(t, nil, err) @@ -1125,6 +1144,7 @@ func Test_Ctx_SaveFile(t *testing.T) { bs, err := ioutil.ReadFile(tempFile.Name()) utils.AssertEqual(t, nil, err) utils.AssertEqual(t, "hello world", string(bs)) + return nil }) body := &bytes.Buffer{} @@ -1150,32 +1170,32 @@ func Test_Ctx_SaveFile(t *testing.T) { func Test_Ctx_Secure(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) // TODO Add TLS conn - utils.AssertEqual(t, false, ctx.Secure()) + utils.AssertEqual(t, false, c.Secure()) } // go test -run Test_Ctx_Stale func Test_Ctx_Stale(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - utils.AssertEqual(t, true, ctx.Stale()) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + utils.AssertEqual(t, true, c.Stale()) } // go test -run Test_Ctx_Subdomains func Test_Ctx_Subdomains(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.Fasthttp.Request.URI().SetHost("john.doe.is.awesome.google.com") - utils.AssertEqual(t, []string{"john", "doe"}, ctx.Subdomains(4)) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().URI().SetHost("john.doe.is.awesome.google.com") + utils.AssertEqual(t, []string{"john", "doe"}, c.Subdomains(4)) - ctx.Fasthttp.Request.URI().SetHost("localhost:3000") - utils.AssertEqual(t, []string{"localhost:3000"}, ctx.Subdomains()) + c.Request().URI().SetHost("localhost:3000") + utils.AssertEqual(t, []string{"localhost:3000"}, c.Subdomains()) } // go test -v -run=^$ -bench=Benchmark_Ctx_Subdomains -benchmem -count=4 @@ -1183,7 +1203,7 @@ func Benchmark_Ctx_Subdomains(b *testing.B) { app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) - c.Fasthttp.Request.SetRequestURI("http://john.doe.google.com") + c.Request().SetRequestURI("http://john.doe.google.com") var res []string b.ReportAllocs() b.ResetTimer() @@ -1197,27 +1217,27 @@ func Benchmark_Ctx_Subdomains(b *testing.B) { func Test_Ctx_ClearCookie(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.Fasthttp.Request.Header.Set(HeaderCookie, "john=doe") - ctx.ClearCookie("john") - utils.AssertEqual(t, true, strings.HasPrefix(string(ctx.Fasthttp.Response.Header.Peek(HeaderSetCookie)), "john=; expires=")) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderCookie, "john=doe") + c.ClearCookie("john") + utils.AssertEqual(t, true, strings.HasPrefix(string(c.Response().Header.Peek(HeaderSetCookie)), "john=; expires=")) - ctx.Fasthttp.Request.Header.Set(HeaderCookie, "test1=dummy") - ctx.Fasthttp.Request.Header.Set(HeaderCookie, "test2=dummy") - ctx.ClearCookie() - utils.AssertEqual(t, true, strings.Contains(string(ctx.Fasthttp.Response.Header.Peek(HeaderSetCookie)), "test1=; expires=")) - utils.AssertEqual(t, true, strings.Contains(string(ctx.Fasthttp.Response.Header.Peek(HeaderSetCookie)), "test2=; expires=")) + c.Request().Header.Set(HeaderCookie, "test1=dummy") + c.Request().Header.Set(HeaderCookie, "test2=dummy") + c.ClearCookie() + utils.AssertEqual(t, true, strings.Contains(string(c.Response().Header.Peek(HeaderSetCookie)), "test1=; expires=")) + utils.AssertEqual(t, true, strings.Contains(string(c.Response().Header.Peek(HeaderSetCookie)), "test2=; expires=")) } // go test -race -run Test_Ctx_Download func Test_Ctx_Download(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) - ctx.Download("ctx.go", "Awesome File!") + c.Download("ctx.go", "Awesome File!") f, err := os.Open("./ctx.go") utils.AssertEqual(t, nil, err) @@ -1225,8 +1245,8 @@ func Test_Ctx_Download(t *testing.T) { expect, err := ioutil.ReadAll(f) utils.AssertEqual(t, nil, err) - utils.AssertEqual(t, expect, ctx.Fasthttp.Response.Body()) - utils.AssertEqual(t, `attachment; filename="Awesome+File%21"`, string(ctx.Fasthttp.Response.Header.Peek(HeaderContentDisposition))) + utils.AssertEqual(t, expect, c.Response().Body()) + utils.AssertEqual(t, `attachment; filename="Awesome+File%21"`, string(c.Response().Header.Peek(HeaderContentDisposition))) } // go test -race -run Test_Ctx_SendFile @@ -1245,41 +1265,42 @@ func Test_Ctx_SendFile(t *testing.T) { utils.AssertEqual(t, nil, err) // simple test case - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - err = ctx.SendFile("ctx.go") + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + err = c.SendFile("ctx.go") // check expectation utils.AssertEqual(t, nil, err) - utils.AssertEqual(t, expectFileContent, ctx.Fasthttp.Response.Body()) - utils.AssertEqual(t, StatusOK, ctx.Fasthttp.Response.StatusCode()) - app.ReleaseCtx(ctx) + utils.AssertEqual(t, expectFileContent, c.Response().Body()) + utils.AssertEqual(t, StatusOK, c.Response().StatusCode()) + app.ReleaseCtx(c) // test with custom error code - ctx = app.AcquireCtx(&fasthttp.RequestCtx{}) - err = ctx.Status(StatusInternalServerError).SendFile("ctx.go") + c = app.AcquireCtx(&fasthttp.RequestCtx{}) + err = c.Status(StatusInternalServerError).SendFile("ctx.go") // check expectation utils.AssertEqual(t, nil, err) - utils.AssertEqual(t, expectFileContent, ctx.Fasthttp.Response.Body()) - utils.AssertEqual(t, StatusInternalServerError, ctx.Fasthttp.Response.StatusCode()) - app.ReleaseCtx(ctx) + utils.AssertEqual(t, expectFileContent, c.Response().Body()) + utils.AssertEqual(t, StatusInternalServerError, c.Response().StatusCode()) + app.ReleaseCtx(c) // test not modified - ctx = app.AcquireCtx(&fasthttp.RequestCtx{}) - ctx.Fasthttp.Request.Header.Set(HeaderIfModifiedSince, fI.ModTime().Format(time.RFC1123)) - err = ctx.SendFile("ctx.go") + c = app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().Header.Set(HeaderIfModifiedSince, fI.ModTime().Format(time.RFC1123)) + err = c.SendFile("ctx.go") // check expectation utils.AssertEqual(t, nil, err) - utils.AssertEqual(t, StatusNotModified, ctx.Fasthttp.Response.StatusCode()) - utils.AssertEqual(t, []byte(nil), ctx.Fasthttp.Response.Body()) - app.ReleaseCtx(ctx) + utils.AssertEqual(t, StatusNotModified, c.Response().StatusCode()) + utils.AssertEqual(t, []byte(nil), c.Response().Body()) + app.ReleaseCtx(c) } // go test -race -run Test_Ctx_SendFile_404 func Test_Ctx_SendFile_404(t *testing.T) { t.Parallel() app := New() - app.Get("/", func(ctx *Ctx) { - err := ctx.SendFile("./john_dow.go/") + app.Get("/", func(c *Ctx) error { + err := c.SendFile("./john_dow.go/") utils.AssertEqual(t, false, err == nil) + return nil }) resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) @@ -1291,13 +1312,13 @@ func Test_Ctx_SendFile_404(t *testing.T) { func Test_Ctx_SendFile_Immutable(t *testing.T) { t.Parallel() app := New() - app.Get("/:file", func(c *Ctx) { + app.Get("/:file", func(c *Ctx) error { file := c.Params("file") if err := c.SendFile("./.github/" + file + ".html"); err != nil { utils.AssertEqual(t, nil, err) } utils.AssertEqual(t, "index", fmt.Sprintf("%s", file)) - c.Send(file) + return c.SendString(file) }) // 1st try resp, err := app.Test(httptest.NewRequest("GET", "/index", nil)) @@ -1313,22 +1334,22 @@ func Test_Ctx_SendFile_Immutable(t *testing.T) { func Test_Ctx_JSON(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) - utils.AssertEqual(t, true, ctx.JSON(complex(1, 1)) != nil) + utils.AssertEqual(t, true, c.JSON(complex(1, 1)) != nil) - ctx.JSON(Map{ // map has no order + c.JSON(Map{ // map has no order "Name": "Grame", "Age": 20, }) - utils.AssertEqual(t, `{"Age":20,"Name":"Grame"}`, string(ctx.Fasthttp.Response.Body())) - utils.AssertEqual(t, "application/json", string(ctx.Fasthttp.Response.Header.Peek("content-type"))) + utils.AssertEqual(t, `{"Age":20,"Name":"Grame"}`, string(c.Response().Body())) + utils.AssertEqual(t, "application/json", string(c.Response().Header.Peek("content-type"))) testEmpty := func(v interface{}, r string) { - err := ctx.JSON(v) + err := c.JSON(v) utils.AssertEqual(t, nil, err) - utils.AssertEqual(t, r, string(ctx.Fasthttp.Response.Body())) + utils.AssertEqual(t, r, string(c.Response().Body())) } testEmpty(nil, "null") @@ -1337,7 +1358,7 @@ func Test_Ctx_JSON(t *testing.T) { testEmpty([]int{}, "[]") } -// go test -v -run=^$ -bench=Benchmark_Ctx_JSON -benchmem -count=4 +// go test -run=^$ -bench=Benchmark_Ctx_JSON -benchmem -count=4 func Benchmark_Ctx_JSON(b *testing.B) { app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) @@ -1357,31 +1378,31 @@ func Benchmark_Ctx_JSON(b *testing.B) { err = c.JSON(data) } utils.AssertEqual(b, nil, err) - utils.AssertEqual(b, `{"Name":"Grame","Age":20}`, string(c.Fasthttp.Response.Body())) + utils.AssertEqual(b, `{"Name":"Grame","Age":20}`, string(c.Response().Body())) } // go test -run Test_Ctx_JSONP func Test_Ctx_JSONP(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) - utils.AssertEqual(t, true, ctx.JSONP(complex(1, 1)) != nil) + utils.AssertEqual(t, true, c.JSONP(complex(1, 1)) != nil) - ctx.JSONP(Map{ + c.JSONP(Map{ "Name": "Grame", "Age": 20, }) - utils.AssertEqual(t, `callback({"Age":20,"Name":"Grame"});`, string(ctx.Fasthttp.Response.Body())) - utils.AssertEqual(t, "application/javascript; charset=utf-8", string(ctx.Fasthttp.Response.Header.Peek("content-type"))) + utils.AssertEqual(t, `callback({"Age":20,"Name":"Grame"});`, string(c.Response().Body())) + utils.AssertEqual(t, "application/javascript; charset=utf-8", string(c.Response().Header.Peek("content-type"))) - ctx.JSONP(Map{ + c.JSONP(Map{ "Name": "Grame", "Age": 20, }, "john") - utils.AssertEqual(t, `john({"Age":20,"Name":"Grame"});`, string(ctx.Fasthttp.Response.Body())) - utils.AssertEqual(t, "application/javascript; charset=utf-8", string(ctx.Fasthttp.Response.Header.Peek("content-type"))) + utils.AssertEqual(t, `john({"Age":20,"Name":"Grame"});`, string(c.Response().Body())) + utils.AssertEqual(t, "application/javascript; charset=utf-8", string(c.Response().Header.Peek("content-type"))) } // go test -v -run=^$ -bench=Benchmark_Ctx_JSONP -benchmem -count=4 @@ -1405,24 +1426,24 @@ func Benchmark_Ctx_JSONP(b *testing.B) { err = c.JSONP(data, callback) } utils.AssertEqual(b, nil, err) - utils.AssertEqual(b, `emit({"Name":"Grame","Age":20});`, string(c.Fasthttp.Response.Body())) + utils.AssertEqual(b, `emit({"Name":"Grame","Age":20});`, string(c.Response().Body())) } // go test -run Test_Ctx_Links func Test_Ctx_Links(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) - ctx.Links() - utils.AssertEqual(t, "", string(ctx.Fasthttp.Response.Header.Peek(HeaderLink))) + c.Links() + utils.AssertEqual(t, "", string(c.Response().Header.Peek(HeaderLink))) - ctx.Links( + c.Links( "http://api.example.com/users?page=2", "next", "http://api.example.com/users?page=5", "last", ) - utils.AssertEqual(t, `; rel="next",; rel="last"`, string(ctx.Fasthttp.Response.Header.Peek(HeaderLink))) + utils.AssertEqual(t, `; rel="next",; rel="last"`, string(c.Response().Header.Peek(HeaderLink))) } // go test -v -run=^$ -bench=Benchmark_Ctx_Links -benchmem -count=4 @@ -1444,20 +1465,21 @@ func Benchmark_Ctx_Links(b *testing.B) { func Test_Ctx_Location(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.Location("http://example.com") - utils.AssertEqual(t, "http://example.com", string(ctx.Fasthttp.Response.Header.Peek(HeaderLocation))) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Location("http://example.com") + utils.AssertEqual(t, "http://example.com", string(c.Response().Header.Peek(HeaderLocation))) } // go test -run Test_Ctx_Next func Test_Ctx_Next(t *testing.T) { app := New() - app.Use("/", func(c *Ctx) { - c.Next() + app.Use("/", func(c *Ctx) error { + return c.Next() }) - app.Get("/test", func(c *Ctx) { + app.Get("/test", func(c *Ctx) error { c.Set("X-Next-Result", "Works") + return nil }) resp, err := app.Test(httptest.NewRequest(MethodGet, "http://example.com/test", nil)) utils.AssertEqual(t, nil, err, "app.Test(req)") @@ -1468,9 +1490,9 @@ func Test_Ctx_Next(t *testing.T) { // go test -run Test_Ctx_Next_Error func Test_Ctx_Next_Error(t *testing.T) { app := New() - app.Use("/", func(c *Ctx) { + app.Use("/", func(c *Ctx) error { c.Set("X-Next-Result", "Works") - c.Next(ErrNotFound) + return ErrNotFound }) resp, err := app.Test(httptest.NewRequest(MethodGet, "http://example.com/test", nil)) @@ -1483,25 +1505,25 @@ func Test_Ctx_Next_Error(t *testing.T) { func Test_Ctx_Redirect(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) - ctx.Redirect("http://default.com") - utils.AssertEqual(t, 302, ctx.Fasthttp.Response.StatusCode()) - utils.AssertEqual(t, "http://default.com", string(ctx.Fasthttp.Response.Header.Peek(HeaderLocation))) + c.Redirect("http://default.com") + utils.AssertEqual(t, 302, c.Response().StatusCode()) + utils.AssertEqual(t, "http://default.com", string(c.Response().Header.Peek(HeaderLocation))) - ctx.Redirect("http://example.com", 301) - utils.AssertEqual(t, 301, ctx.Fasthttp.Response.StatusCode()) - utils.AssertEqual(t, "http://example.com", string(ctx.Fasthttp.Response.Header.Peek(HeaderLocation))) + c.Redirect("http://example.com", 301) + utils.AssertEqual(t, 301, c.Response().StatusCode()) + utils.AssertEqual(t, "http://example.com", string(c.Response().Header.Peek(HeaderLocation))) } // go test -run Test_Ctx_Render func Test_Ctx_Render(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - err := ctx.Render("./.github/TEST_DATA/template.html", Map{ + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + err := c.Render("./.github/testdata/template.html", Map{ "Title": "Hello, World!", }) @@ -1510,12 +1532,12 @@ func Test_Ctx_Render(t *testing.T) { defer bytebufferpool.Put(buf) utils.AssertEqual(t, nil, err) - utils.AssertEqual(t, "

Hello, World!

", string(ctx.Fasthttp.Response.Body())) + utils.AssertEqual(t, "

Hello, World!

", string(c.Response().Body())) - err = ctx.Render("./.github/TEST_DATA/template-non-exists.html", nil) + err = c.Render("./.github/testdata/template-non-exists.html", nil) utils.AssertEqual(t, false, err == nil) - err = ctx.Render("./.github/TEST_DATA/template-invalid.html", nil) + err = c.Render("./.github/testdata/template-invalid.html", nil) utils.AssertEqual(t, false, err == nil) } @@ -1529,7 +1551,7 @@ func (t *testTemplateEngine) Render(w io.Writer, name string, bind interface{}, } func (t *testTemplateEngine) Load() error { - t.templates = template.Must(template.ParseGlob("./.github/TEST_DATA/*.tmpl")) + t.templates = template.Must(template.ParseGlob("./.github/testdata/*.tmpl")) return nil } @@ -1538,14 +1560,14 @@ func Test_Ctx_Render_Engine(t *testing.T) { engine := &testTemplateEngine{} engine.Load() app := New() - app.Settings.Views = engine - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - err := ctx.Render("index.tmpl", Map{ + app.config.Views = engine + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + err := c.Render("index.tmpl", Map{ "Title": "Hello, World!", }) utils.AssertEqual(t, nil, err) - utils.AssertEqual(t, "

Hello, World!

", string(ctx.Fasthttp.Response.Body())) + utils.AssertEqual(t, "

Hello, World!

", string(c.Response().Body())) } // go test -v -run=^$ -bench=Benchmark_Ctx_Render_Engine -benchmem -count=4 @@ -1554,18 +1576,18 @@ func Benchmark_Ctx_Render_Engine(b *testing.B) { err := engine.Load() utils.AssertEqual(b, nil, err) app := New() - app.Settings.Views = engine - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) + app.config.Views = engine + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - err = ctx.Render("index.tmpl", Map{ + err = c.Render("index.tmpl", Map{ "Title": "Hello, World!", }) } utils.AssertEqual(b, nil, err) - utils.AssertEqual(b, "

Hello, World!

", string(ctx.Fasthttp.Response.Body())) + utils.AssertEqual(b, "

Hello, World!

", string(c.Response().Body())) } type errorTemplateEngine struct{} @@ -1579,10 +1601,10 @@ func (t errorTemplateEngine) Load() error { return nil } // go test -run Test_Ctx_Render_Engine_Error func Test_Ctx_Render_Engine_Error(t *testing.T) { app := New() - app.Settings.Views = errorTemplateEngine{} - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - err := ctx.Render("index.tmpl", nil) + app.config.Views = errorTemplateEngine{} + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + err := c.Render("index.tmpl", nil) utils.AssertEqual(t, false, err == nil) } @@ -1602,24 +1624,24 @@ func Test_Ctx_Render_Go_Template(t *testing.T) { app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) - err = ctx.Render(file.Name(), nil) + err = c.Render(file.Name(), nil) utils.AssertEqual(t, nil, err) - utils.AssertEqual(t, "template", string(ctx.Fasthttp.Response.Body())) + utils.AssertEqual(t, "template", string(c.Response().Body())) } // go test -run Test_Ctx_Send func Test_Ctx_Send(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.Send([]byte("Hello, World")) - ctx.Send("Don't crash please") - ctx.Send(1337) - utils.AssertEqual(t, "1337", string(ctx.Fasthttp.Response.Body())) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Send([]byte("Hello, World")) + c.Send([]byte("Don't crash please")) + c.Send([]byte("1337")) + utils.AssertEqual(t, "1337", string(c.Response().Body())) } // go test -v -run=^$ -bench=Benchmark_Ctx_Send -benchmem -count=4 @@ -1627,133 +1649,133 @@ func Benchmark_Ctx_Send(b *testing.B) { app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) - var str = "Hello, World!" var byt = []byte("Hello, World!") - var nmb = 123 - var bol = true b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - c.Send(str) c.Send(byt) - c.Send(nmb) - c.Send(bol) } - utils.AssertEqual(b, "true", string(c.Fasthttp.Response.Body())) -} - -// go test -run Test_Ctx_SendBytes -func Test_Ctx_SendBytes(t *testing.T) { - t.Parallel() - app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.SendBytes([]byte("Hello, World!")) - utils.AssertEqual(t, "Hello, World!", string(ctx.Fasthttp.Response.Body())) -} - -// go test -v -run=^$ -bench=Benchmark_Ctx_Benchmark_Ctx_SendBytes -benchmem -count=4 -func Benchmark_Ctx_SendBytes(b *testing.B) { - app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(c) - var byt = []byte("Hello, World!") - b.ReportAllocs() - b.ResetTimer() - for n := 0; n < b.N; n++ { - c.SendBytes(byt) - } - utils.AssertEqual(b, "Hello, World!", string(c.Fasthttp.Response.Body())) + utils.AssertEqual(b, "Hello, World!", string(c.Response().Body())) } // go test -run Test_Ctx_SendStatus func Test_Ctx_SendStatus(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.SendStatus(415) - utils.AssertEqual(t, 415, ctx.Fasthttp.Response.StatusCode()) - utils.AssertEqual(t, "Unsupported Media Type", string(ctx.Fasthttp.Response.Body())) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.SendStatus(415) + utils.AssertEqual(t, 415, c.Response().StatusCode()) + utils.AssertEqual(t, "Unsupported Media Type", string(c.Response().Body())) } // go test -run Test_Ctx_SendString func Test_Ctx_SendString(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.SendString("Don't crash please") - utils.AssertEqual(t, "Don't crash please", string(ctx.Fasthttp.Response.Body())) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.SendString("Don't crash please") + utils.AssertEqual(t, "Don't crash please", string(c.Response().Body())) } // go test -run Test_Ctx_SendStream func Test_Ctx_SendStream(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) - ctx.SendStream(bytes.NewReader([]byte("Don't crash please"))) - utils.AssertEqual(t, "Don't crash please", string(ctx.Fasthttp.Response.Body())) + c.SendStream(bytes.NewReader([]byte("Don't crash please"))) + utils.AssertEqual(t, "Don't crash please", string(c.Response().Body())) - ctx.SendStream(bytes.NewReader([]byte("Don't crash please")), len([]byte("Don't crash please"))) - utils.AssertEqual(t, "Don't crash please", string(ctx.Fasthttp.Response.Body())) + c.SendStream(bytes.NewReader([]byte("Don't crash please")), len([]byte("Don't crash please"))) + utils.AssertEqual(t, "Don't crash please", string(c.Response().Body())) - ctx.SendStream(bufio.NewReader(bytes.NewReader([]byte("Hello bufio")))) - utils.AssertEqual(t, "Hello bufio", string(ctx.Fasthttp.Response.Body())) + c.SendStream(bufio.NewReader(bytes.NewReader([]byte("Hello bufio")))) + utils.AssertEqual(t, "Hello bufio", string(c.Response().Body())) file, err := os.Open("./.github/index.html") utils.AssertEqual(t, nil, err) - ctx.SendStream(bufio.NewReader(file)) - utils.AssertEqual(t, true, (ctx.Fasthttp.Response.Header.ContentLength() > 200)) + c.SendStream(bufio.NewReader(file)) + utils.AssertEqual(t, true, (c.Response().Header.ContentLength() > 200)) } // go test -run Test_Ctx_Set func Test_Ctx_Set(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.Set("X-1", "1") - ctx.Set("X-2", "2") - ctx.Set("X-3", "3") - ctx.Set("X-3", "1337") - utils.AssertEqual(t, "1", string(ctx.Fasthttp.Response.Header.Peek("x-1"))) - utils.AssertEqual(t, "2", string(ctx.Fasthttp.Response.Header.Peek("x-2"))) - utils.AssertEqual(t, "1337", string(ctx.Fasthttp.Response.Header.Peek("x-3"))) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Set("X-1", "1") + c.Set("X-2", "2") + c.Set("X-3", "3") + c.Set("X-3", "1337") + utils.AssertEqual(t, "1", string(c.Response().Header.Peek("x-1"))) + utils.AssertEqual(t, "2", string(c.Response().Header.Peek("x-2"))) + utils.AssertEqual(t, "1337", string(c.Response().Header.Peek("x-3"))) +} + +// go test -run Test_Ctx_Set_Splitter +func Test_Ctx_Set_Splitter(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + c.Set("Location", "foo\r\nSet-Cookie:%20SESSIONID=MaliciousValue\r\n") + h := string(c.Response().Header.Peek("Location")) + utils.AssertEqual(t, false, strings.Contains(h, "\r\n"), h) + + c.Set("Location", "foo\nSet-Cookie:%20SESSIONID=MaliciousValue\n") + h = string(c.Response().Header.Peek("Location")) + utils.AssertEqual(t, false, strings.Contains(h, "\n"), h) + +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Set -benchmem -count=4 +func Benchmark_Ctx_Set(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + var val = "1431-15132-3423" + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + c.Set(HeaderXRequestID, val) + } } // go test -run Test_Ctx_Status func Test_Ctx_Status(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.Status(400) - utils.AssertEqual(t, 400, ctx.Fasthttp.Response.StatusCode()) - ctx.Status(415).Send("Hello, World") - utils.AssertEqual(t, 415, ctx.Fasthttp.Response.StatusCode()) - utils.AssertEqual(t, "Hello, World", string(ctx.Fasthttp.Response.Body())) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Status(400) + utils.AssertEqual(t, 400, c.Response().StatusCode()) + c.Status(415).Send([]byte("Hello, World")) + utils.AssertEqual(t, 415, c.Response().StatusCode()) + utils.AssertEqual(t, "Hello, World", string(c.Response().Body())) } // go test -run Test_Ctx_Type func Test_Ctx_Type(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.Type(".json") - utils.AssertEqual(t, "application/json", string(ctx.Fasthttp.Response.Header.Peek("Content-Type"))) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Type(".json") + utils.AssertEqual(t, "application/json", string(c.Response().Header.Peek("Content-Type"))) - ctx.Type("json", "utf-8") - utils.AssertEqual(t, "application/json; charset=utf-8", string(ctx.Fasthttp.Response.Header.Peek("Content-Type"))) + c.Type("json", "utf-8") + utils.AssertEqual(t, "application/json; charset=utf-8", string(c.Response().Header.Peek("Content-Type"))) - ctx.Type(".html") - utils.AssertEqual(t, "text/html", string(ctx.Fasthttp.Response.Header.Peek("Content-Type"))) + c.Type(".html") + utils.AssertEqual(t, "text/html", string(c.Response().Header.Peek("Content-Type"))) - ctx.Type("html", "utf-8") - utils.AssertEqual(t, "text/html; charset=utf-8", string(ctx.Fasthttp.Response.Header.Peek("Content-Type"))) + c.Type("html", "utf-8") + utils.AssertEqual(t, "text/html; charset=utf-8", string(c.Response().Header.Peek("Content-Type"))) } // go test -v -run=^$ -bench=Benchmark_Ctx_Type -benchmem -count=4 @@ -1786,12 +1808,12 @@ func Benchmark_Ctx_Type_Charset(b *testing.B) { func Test_Ctx_Vary(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.Vary("Origin") - ctx.Vary("User-Agent") - ctx.Vary("Accept-Encoding", "Accept") - utils.AssertEqual(t, "Origin, User-Agent, Accept-Encoding, Accept", string(ctx.Fasthttp.Response.Header.Peek("Vary"))) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Vary("Origin") + c.Vary("User-Agent") + c.Vary("Accept-Encoding", "Accept") + utils.AssertEqual(t, "Origin, User-Agent, Accept-Encoding, Accept", string(c.Response().Header.Peek("Vary"))) } // go test -v -run=^$ -bench=Benchmark_Ctx_Vary -benchmem -count=4 @@ -1810,15 +1832,11 @@ func Benchmark_Ctx_Vary(b *testing.B) { func Test_Ctx_Write(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.Write("Hello, ") - ctx.Write([]byte("World! ")) - ctx.Write(123) - ctx.Write(123.321) - ctx.Write(true) - ctx.Write(bytes.NewReader([]byte("Don't crash please"))) - utils.AssertEqual(t, "Don't crash please", string(ctx.Fasthttp.Response.Body())) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Write([]byte("Hello, ")) + c.Write([]byte("World!")) + utils.AssertEqual(t, "Hello, World!", string(c.Response().Body())) } // go test -v -run=^$ -bench=Benchmark_Ctx_Write -benchmem -count=4 @@ -1826,17 +1844,11 @@ func Benchmark_Ctx_Write(b *testing.B) { app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) - var str = "Hello, World!" var byt = []byte("Hello, World!") - var nmb = 123 - var bol = true b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - c.Write(str) c.Write(byt) - c.Write(nmb) - c.Write(bol) } } @@ -1844,10 +1856,25 @@ func Benchmark_Ctx_Write(b *testing.B) { func Test_Ctx_XHR(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.Fasthttp.Request.Header.Set(HeaderXRequestedWith, "XMLHttpRequest") - utils.AssertEqual(t, true, ctx.XHR()) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderXRequestedWith, "XMLHttpRequest") + utils.AssertEqual(t, true, c.XHR()) +} + +// go test -run=^$ -bench=Benchmark_Ctx_XHR -benchmem -count=4 +func Benchmark_Ctx_XHR(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderXRequestedWith, "XMLHttpRequest") + var equal bool + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + equal = c.XHR() + } + utils.AssertEqual(b, true, equal) } // go test -v -run=^$ -bench=Benchmark_Ctx_SendString_B -benchmem -count=4 @@ -1861,75 +1888,51 @@ func Benchmark_Ctx_SendString_B(b *testing.B) { for n := 0; n < b.N; n++ { c.SendString(body) } - utils.AssertEqual(b, []byte("Hello, world!"), c.Fasthttp.Response.Body()) -} - -// go test -v -run=^$ -bench=Benchmark_Ctx_SendBytes_B -benchmem -count=4 -func Benchmark_Ctx_SendBytes_B(b *testing.B) { - app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(c) - body := []byte("Hello, world!") - b.ReportAllocs() - b.ResetTimer() - for n := 0; n < b.N; n++ { - c.SendBytes(body) - } - utils.AssertEqual(b, []byte("Hello, world!"), c.Fasthttp.Response.Body()) + utils.AssertEqual(b, []byte("Hello, world!"), c.Response().Body()) } // go test -run Benchmark_Ctx_QueryParser func Test_Ctx_QueryParser(t *testing.T) { t.Parallel() app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) type Query struct { ID int Name string Hobby []string } - ctx.Fasthttp.Request.SetBody([]byte(``)) - ctx.Fasthttp.Request.Header.SetContentType("") - ctx.Fasthttp.Request.URI().SetQueryString("id=1&name=tom&hobby=basketball&hobby=football") + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball&hobby=football") q := new(Query) - utils.AssertEqual(t, nil, ctx.QueryParser(q)) + utils.AssertEqual(t, nil, c.QueryParser(q)) utils.AssertEqual(t, 2, len(q.Hobby)) empty := new(Query) - ctx.Fasthttp.Request.URI().SetQueryString("") - utils.AssertEqual(t, nil, ctx.QueryParser(empty)) + c.Request().URI().SetQueryString("") + utils.AssertEqual(t, nil, c.QueryParser(empty)) utils.AssertEqual(t, 0, len(empty.Hobby)) } // go test -v -run=^$ -bench=Benchmark_Ctx_QueryParser -benchmem -count=4 func Benchmark_Ctx_QueryParser(b *testing.B) { app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) type Query struct { ID int Name string Hobby []string } - ctx.Fasthttp.Request.SetBody([]byte(``)) - ctx.Fasthttp.Request.Header.SetContentType("") - ctx.Fasthttp.Request.URI().SetQueryString("id=1&name=tom&hobby=basketball&hobby=football") + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball&hobby=football") q := new(Query) b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - ctx.QueryParser(q) + c.QueryParser(q) } - utils.AssertEqual(b, nil, ctx.QueryParser(q)) -} - -// go test -run Test_Ctx_Error -func Test_Ctx_Error(t *testing.T) { - t.Parallel() - app := New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(ctx) - ctx.Next(fmt.Errorf("Hi I'm an error!")) - utils.AssertEqual(t, "Hi I'm an error!", ctx.Error().Error()) + utils.AssertEqual(b, nil, c.QueryParser(q)) } diff --git a/go.mod b/go.mod index 22438a19..d92be4cc 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,8 @@ -module github.com/gofiber/fiber +module github.com/gofiber/fiber/v2 -go 1.11 +go 1.14 require ( - github.com/gofiber/utils v0.0.10 - github.com/gorilla/schema v1.1.0 - github.com/mattn/go-colorable v0.1.7 - github.com/mattn/go-isatty v0.0.12 - github.com/valyala/bytebufferpool v1.0.0 github.com/valyala/fasthttp v1.16.0 + golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 ) diff --git a/go.sum b/go.sum index 4a707320..57d2d8ee 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,7 @@ github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4= github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= -github.com/gofiber/utils v0.0.10 h1:3Mr7X7JdCUo7CWf/i5sajSaDmArEDtti8bM1JUVso2U= -github.com/gofiber/utils v0.0.10/go.mod h1:9J5aHFUIjq0XfknT4+hdSMG6/jzfaAgCu4HEbWDeBlo= -github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY= -github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/klauspost/compress v1.10.7 h1:7rix8v8GpI3ZBb0nSozFRgbtXKv+hOe+qfEpZqybrAg= github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= -github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.16.0 h1:9zAqOYLl8Tuy3E5R6ckzGDJ1g8+pw15oQp2iL9Jl6gQ= @@ -18,12 +10,12 @@ github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a h1:0R4NLDRDZX6Jc github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/group.go b/group.go index 9727a431..1c2abc37 100644 --- a/group.go +++ b/group.go @@ -15,38 +15,51 @@ type Group struct { prefix string } -// Use registers a middleware route. -// Middleware matches requests beginning with the provided prefix. -// Providing a prefix is optional, it defaults to "/". +// Use registers a middleware route. that will match requests +// that contain the provided prefix ( which is optional and defaults to "/" ). // -// - group.Use(handler) -// - group.Use("/api", handler) -// - group.Use("/api", handler, handler) +// app.Use(func(c *fiber.Ctx) error { +// return c.Next() +// }) +// app.Use("/api", func(c *fiber.Ctx) error { +// return c.Next() +// }) +// app.Use("/api", handler(), func(c *fiber.Ctx) error { +// return c.Next() +// }) +// +// This method will match all HTTP verbs: GET, POST, PUT, HEAD etc... func (grp *Group) Use(args ...interface{}) Router { - var path = "" + var prefix = "" var handlers []Handler for i := 0; i < len(args); i++ { switch arg := args[i].(type) { case string: - path = arg + prefix = arg case Handler: handlers = append(handlers, arg) + case *App: + stack := arg.Stack() + for m := range stack { + for r := range stack[m] { + route := grp.app.copyRoute(stack[m][r]) + grp.app.addRoute(route.Method, grp.app.addPrefixToRoute(prefix, route)) + } + } + return grp.app default: panic(fmt.Sprintf("use: invalid handler %v\n", reflect.TypeOf(arg))) } } - grp.app.register(methodUse, getGroupPath(grp.prefix, path), handlers...) + grp.app.register(methodUse, getGroupPath(grp.prefix, prefix), handlers...) return grp } // Get registers a route for GET methods that requests a representation // of the specified resource. Requests using GET should only retrieve data. func (grp *Group) Get(path string, handlers ...Handler) Router { - route := grp.app.register(MethodGet, getGroupPath(grp.prefix, path), handlers...) - // Add head route - headRoute := route - grp.app.addRoute(MethodHead, &headRoute) - return grp + path = getGroupPath(grp.prefix, path) + return grp.app.Add(MethodHead, path, handlers...).Add(MethodGet, path, handlers...) } // Head registers a route for HEAD methods that asks for a response identical @@ -96,31 +109,31 @@ func (grp *Group) Patch(path string, handlers ...Handler) Router { return grp.Add(MethodPatch, path, handlers...) } -// Add ... +// Add allows you to specify a HTTP method to register a route func (grp *Group) Add(method, path string, handlers ...Handler) Router { - grp.app.register(method, getGroupPath(grp.prefix, path), handlers...) - return grp + return grp.app.register(method, getGroupPath(grp.prefix, path), handlers...) } -// Static ... +// Static will create a file server serving static files func (grp *Group) Static(prefix, root string, config ...Static) Router { - grp.app.registerStatic(getGroupPath(grp.prefix, prefix), root, config...) - return grp + return grp.app.registerStatic(getGroupPath(grp.prefix, prefix), root, config...) } -// All ... +// All will register the handler on all HTTP methods func (grp *Group) All(path string, handlers ...Handler) Router { for _, method := range intMethod { - grp.Add(method, path, handlers...) + _ = grp.Add(method, path, handlers...) } return grp } // Group is used for Routes with common prefix to define a new sub-router with optional middleware. +// api := app.Group("/api") +// api.Get("/users", handler()) func (grp *Group) Group(prefix string, handlers ...Handler) Router { prefix = getGroupPath(grp.prefix, prefix) if len(handlers) > 0 { - grp.app.register(methodUse, prefix, handlers...) + _ = grp.app.register(methodUse, prefix, handlers...) } return grp.app.Group(prefix) } diff --git a/middleware/README.md b/middleware/README.md deleted file mode 100644 index 9d4ae971..00000000 --- a/middleware/README.md +++ /dev/null @@ -1,16 +0,0 @@ -## Fiber Core Middleware - -- [middleware/compress](compress.md) -Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli`. -- [middleware/favicon](favicon.md) -This middleware caches the favicon in memory to improve performance -- [middleware/filesystem](filesystem.md) -FileServer middleware to allow embedded http.FileSystem -- [middleware/logger](logger.md) -HTTP request/response logger for Fiber -- [middleware/recover](recover.md) -Recover middleware recovers from panics anywhere in the stack chain -- [middleware/requestid](request_id.md) -Adds an indentifier to the response using the `X-Request-ID` header -- [middleware/timeout](timeout.md) -Wrapper function which provides a handler with a timeout. \ No newline at end of file diff --git a/middleware/basicauth/README.md b/middleware/basicauth/README.md new file mode 100644 index 00000000..8015b602 --- /dev/null +++ b/middleware/basicauth/README.md @@ -0,0 +1,118 @@ +# Basic Authentication +Basic Authentication middleware for [Fiber](https://github.com/gofiber/fiber) that provides an HTTP basic authentication. It calls the next handler for valid credentials and [401 Unauthorized](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401) or a custom response for missing or invalid credentials. + +### Table of Contents +- [Signatures](#signatures) +- [Examples](#examples) +- [Config](#config) +- [Default Config](#default-config) + + +### Signatures +```go +func New(config Config) fiber.Handler +``` + +### Examples +Import the middleware package that is part of the Fiber web framework +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/basicauth" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: +```go +// Provide a minimal config +app.Use(basicauth.New(basicauth.Config{ + Users: map[string]string{ + "john": "doe", + "admin": "123456", + }, +})) + +// Or extend your config for customization +app.Use(basicauth.New(basicauth.Config{ + Users: map[string]string{ + "john": "doe", + "admin": "123456", + }, + Realm: "Forbidden", + Authorizer: func(user, pass string) bool { + if user == "john" && pass == "doe" { + return true + } + if user == "admin" && pass == "123456" { + return true + } + return false + }, + Unauthorized: func(c *fiber.Ctx) error { + return c.SendFile("./unauthorized.html") + }, + ContextUsername: "_user", + ContextPassword: "_pass", +})) +``` + +### Config +```go +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Users defines the allowed credentials + // + // Required. Default: map[string]string{} + Users map[string]string + + // Realm is a string to define realm attribute of BasicAuth. + // the realm identifies the system to authenticate against + // and can be used by clients to save credentials + // + // Optional. Default: "Restricted". + Realm string + + // Authorizer defines a function you can pass + // to check the credentials however you want. + // It will be called with a username and password + // and is expected to return true or false to indicate + // that the credentials were approved or not. + // + // Optional. Default: nil. + Authorizer func(string, string) bool + + // Unauthorized defines the response body for unauthorized responses. + // By default it will return with a 401 Unauthorized and the correct WWW-Auth header + // + // Optional. Default: nil + Unauthorized fiber.Handler + + // ContextUser is the key to store the username in Locals + // + // Optional. Default: "username" + ContextUsername string + + // ContextPass is the key to store the password in Locals + // + // Optional. Default: "password" + ContextPassword string +} +``` + +### Default Config +```go +var ConfigDefault = Config{ + Next: nil, + Users: map[string]string{}, + Realm: "Restricted", + Authorizer: nil, + Unauthorized: nil, + ContextUsername: "username", + ContextPassword: "password", +} +``` diff --git a/middleware/basicauth/basicauth.go b/middleware/basicauth/basicauth.go new file mode 100644 index 00000000..2100659f --- /dev/null +++ b/middleware/basicauth/basicauth.go @@ -0,0 +1,142 @@ +package basicauth + +import ( + "encoding/base64" + "strings" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Users defines the allowed credentials + // + // Required. Default: map[string]string{} + Users map[string]string + + // Realm is a string to define realm attribute of BasicAuth. + // the realm identifies the system to authenticate against + // and can be used by clients to save credentials + // + // Optional. Default: "Restricted". + Realm string + + // Authorizer defines a function you can pass + // to check the credentials however you want. + // It will be called with a username and password + // and is expected to return true or false to indicate + // that the credentials were approved or not. + // + // Optional. Default: nil. + Authorizer func(string, string) bool + + // Unauthorized defines the response body for unauthorized responses. + // By default it will return with a 401 Unauthorized and the correct WWW-Auth header + // + // Optional. Default: nil + Unauthorized fiber.Handler + + // ContextUser is the key to store the username in Locals + // + // Optional. Default: "username" + ContextUsername string + + // ContextPass is the key to store the password in Locals + // + // Optional. Default: "password" + ContextPassword string +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Next: nil, + Users: map[string]string{}, + Realm: "Restricted", + Authorizer: nil, + Unauthorized: nil, + ContextUsername: "username", + ContextPassword: "password", +} + +// New creates a new middleware handler +func New(config Config) fiber.Handler { + cfg := config + + // Set default values + if cfg.Next == nil { + cfg.Next = ConfigDefault.Next + } + if cfg.Users == nil { + cfg.Users = ConfigDefault.Users + } + if cfg.Realm == "" { + cfg.Realm = ConfigDefault.Realm + } + if cfg.Authorizer == nil { + cfg.Authorizer = func(user, pass string) bool { + user, exist := cfg.Users[user] + if !exist { + return false + } + return user == pass + } + } + if cfg.Unauthorized == nil { + cfg.Unauthorized = func(c *fiber.Ctx) error { + c.Set(fiber.HeaderWWWAuthenticate, "basic realm="+cfg.Realm) + return c.SendStatus(fiber.StatusUnauthorized) + } + } + if cfg.ContextUsername == "" { + cfg.ContextUsername = ConfigDefault.ContextUsername + } + if cfg.ContextPassword == "" { + cfg.ContextPassword = ConfigDefault.ContextPassword + } + + // Return new handler + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Get authorization header + auth := c.Get(fiber.HeaderAuthorization) + + // Check if header is valid + if len(auth) > 6 && strings.ToLower(auth[:5]) == "basic" { + + // Try to decode + if raw, err := base64.StdEncoding.DecodeString(auth[6:]); err == nil { + + // Convert to string + cred := utils.GetString(raw) + + // Find semicolumn + for i := 0; i < len(cred); i++ { + if cred[i] == ':' { + // Split into user & pass + user := cred[:i] + pass := cred[i+1:] + + // If exist & match in Users, we let him pass + if cfg.Authorizer(user, pass) { + c.Locals(cfg.ContextUsername, user) + c.Locals(cfg.ContextPassword, pass) + return c.Next() + } + } + } + } + } + // Authentication failed + return cfg.Unauthorized(c) + } +} diff --git a/middleware/basicauth/basicauth_test.go b/middleware/basicauth/basicauth_test.go new file mode 100644 index 00000000..2becceae --- /dev/null +++ b/middleware/basicauth/basicauth_test.go @@ -0,0 +1,78 @@ +package basicauth + +import ( + "fmt" + "io/ioutil" + "net/http/httptest" + "testing" + + b64 "encoding/base64" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +func Test_Middleware_BasicAuth(t *testing.T) { + app := fiber.New() + + cfg := Config{ + Users: map[string]string{ + "john": "doe", + "admin": "123456", + }, + } + + app.Use(New(cfg)) + + app.Get("/testauth", func(c *fiber.Ctx) error { + username := c.Locals("username").(string) + password := c.Locals("password").(string) + + return c.SendString(username + password) + }) + + tests := []struct { + url string + statusCode int + username string + password string + }{ + { + url: "/testauth", + statusCode: 200, + username: "john", + password: "doe", + }, + { + url: "/testauth", + statusCode: 200, + username: "admin", + password: "123456", + }, + { + url: "/testauth", + statusCode: 401, + username: "ee", + password: "123456", + }, + } + + for _, tt := range tests { + // Base64 encode credentials for http auth header + creds := b64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", tt.username, tt.password))) + + req := httptest.NewRequest("GET", "/testauth", nil) + req.Header.Add("Authorization", "Basic "+creds) + resp, err := app.Test(req) + + body, err := ioutil.ReadAll(resp.Body) + + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, tt.statusCode, resp.StatusCode) + + // Only check body if statusCode is 200 + if tt.statusCode == 200 { + utils.AssertEqual(t, fmt.Sprintf("%s%s", tt.username, tt.password), string(body)) + } + } +} diff --git a/middleware/compress.go b/middleware/compress.go deleted file mode 100644 index 6329cf98..00000000 --- a/middleware/compress.go +++ /dev/null @@ -1,86 +0,0 @@ -package middleware - -import ( - fiber "github.com/gofiber/fiber" - fasthttp "github.com/valyala/fasthttp" -) - -// CompressConfig defines the config for Compress middleware. -type CompressConfig struct { - // Next defines a function to skip this middleware if returned true. - Next func(ctx *fiber.Ctx) bool - // Compression level for brotli, gzip and deflate - Level int -} - -// Compression levels determine the compression complexity -const ( - CompressLevelDisabled = -1 - CompressLevelDefault = 0 - CompressLevelBestSpeed = 1 - CompressLevelBestCompression = 2 -) - -// CompressConfigDefault is the default config -var CompressConfigDefault = CompressConfig{ - Next: nil, - Level: CompressLevelDefault, -} - -var compressHandlers = map[int]fasthttp.RequestHandler{ - CompressLevelDisabled: fasthttp.CompressHandlerBrotliLevel(func(c *fasthttp.RequestCtx) {}, fasthttp.CompressBrotliNoCompression, fasthttp.CompressNoCompression), - CompressLevelDefault: fasthttp.CompressHandlerBrotliLevel(func(c *fasthttp.RequestCtx) {}, fasthttp.CompressBrotliDefaultCompression, fasthttp.CompressDefaultCompression), - CompressLevelBestSpeed: fasthttp.CompressHandlerBrotliLevel(func(c *fasthttp.RequestCtx) {}, fasthttp.CompressBrotliBestSpeed, fasthttp.CompressBestSpeed), - CompressLevelBestCompression: fasthttp.CompressHandlerBrotliLevel(func(c *fasthttp.RequestCtx) {}, fasthttp.CompressBrotliBestCompression, fasthttp.CompressBestCompression), -} - -/* -Compress allows the following config arguments in any order: - - Compress() - - Compress(next func(*fiber.Ctx) bool) - - Compress(level int) - - Compress(config CompressConfig) -*/ -func Compress(options ...interface{}) fiber.Handler { - // Create default config - var config = CompressConfigDefault - // Assert options if provided to adjust the config - if len(options) > 0 { - for i := range options { - switch opt := options[i].(type) { - case func(*fiber.Ctx) bool: - config.Next = opt - case int: - config.Level = opt - case CompressConfig: - config = opt - default: - panic("Compress: the following option types are allowed: int, func(*fiber.Ctx) bool, CompressConfig") - } - } - } - // Return CompressWithConfig - return compress(config) -} - -func compress(config CompressConfig) fiber.Handler { - // Init middleware settings - compressHandler, ok := compressHandlers[config.Level] - if !ok { - // Use default level if provided level is invalid - compressHandler = compressHandlers[CompressLevelDefault] - } - - // Return handler - return func(c *fiber.Ctx) { - // Don't execute the middleware if Next returns false - if config.Next != nil && config.Next(c) { - c.Next() - return - } - // Middleware logic... - c.Next() - // Compress response - compressHandler(c.Fasthttp) - } -} diff --git a/middleware/compress.md b/middleware/compress.md deleted file mode 100644 index bf2677b6..00000000 --- a/middleware/compress.md +++ /dev/null @@ -1,55 +0,0 @@ -# Compress -Compression middleware for Fiber with support for `deflate`, `gzip` and `brotli`. - -### Example -Import the middleware package that is part of the Fiber web framework -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) -``` - -After you initiate your Fiber app, you can use the following possibilities: -```go -// Default compression config -app.Use(middleware.Compress()) - -// Provide a custom compression level -app.Use(middleware.Compress(middleware.CompressLevelBestSpeed)) - -// Provide a full CompressConfig -app.Use(middleware.Compress(middleware.CompressConfig{ - Next: func(c *fiber.Ctx) bool { - return c.Path() == "/ignore" - }, - Level: CompressLevelDefault, -}) -``` - -### Signatures -```go -func Compress(options ...interface{}) fiber.Handler {} -``` - -### Config -```go -type CompressConfig struct { - // Next defines a function to skip this middleware. - // Default: nil - Next func(*fiber.Ctx) bool - - // Compression level for brotli, gzip and deflate - // Default: CompressLevelDefault - Level int -} -``` -### Compression Levels -```go -const ( - CompressLevelDisabled = -1 - CompressLevelDefault = 0 - CompressLevelBestSpeed = 1 - CompressLevelBestCompression = 2 -) -``` \ No newline at end of file diff --git a/middleware/compress/README.md b/middleware/compress/README.md new file mode 100644 index 00000000..3168a0f2 --- /dev/null +++ b/middleware/compress/README.md @@ -0,0 +1,88 @@ +# Compress +Compression middleware for [Fiber](https://github.com/gofiber/fiber) that will compress the response using `gzip`, `deflate` and `brotli` compression depending on the [Accept-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding) header. + +- [Signatures](#signatures) +- [Examples](#examples) +- [Config](#config) +- [Default Config](#default-config) +- [Constants](#config) + + + +### Signatures +```go +func New(config ...Config) fiber.Handler +``` + +### Example +Import the compress package that is part of the Fiber web framework +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/compress" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: +```go +// Default middleware config +app.Use(compress.New()) + +// Provide a custom compression level +app.Use(compress.New(compress.Config{ + Level: compress.LevelBestSpeed, // 1 +})) + +// Skip middleware for specific routes +app.Use(compress.New(compress.Config{ + Next: func(c *fiber.Ctx) bool { + return c.Path() == "/dont_compress" + }, + Level: compress.LevelBestSpeed, // 1 +})) +``` + +### Config +```go +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // CompressLevel determines the compression algoritm + // + // Optional. Default: LevelDefault + // LevelDisabled: -1 + // LevelDefault: 0 + // LevelBestSpeed: 1 + // LevelBestCompression: 2 + Level int +} +``` + +### Default Config +```go +var ConfigDefault = Config{ + Next: nil, + Level: LevelDefault, +} +``` + +### Constants +```go +// Compression levels +const ( + LevelDisabled = -1 + LevelDefault = 0 + LevelBestSpeed = 1 + LevelBestCompression = 2 +) +``` \ No newline at end of file diff --git a/middleware/compress/compress.go b/middleware/compress/compress.go new file mode 100644 index 00000000..05f7d21e --- /dev/null +++ b/middleware/compress/compress.go @@ -0,0 +1,99 @@ +package compress + +import ( + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // CompressLevel determines the compression algoritm + // + // Optional. Default: LevelDefault + // LevelDisabled: -1 + // LevelDefault: 0 + // LevelBestSpeed: 1 + // LevelBestCompression: 2 + Level int +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Next: nil, + Level: LevelDefault, +} + +// Compression levels +const ( + LevelDisabled = -1 + LevelDefault = 0 + LevelBestSpeed = 1 + LevelBestCompression = 2 +) + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + // Set default config + cfg := ConfigDefault + + // Override config if provided + if len(config) > 0 { + cfg = config[0] + + // Set default values + if cfg.Next == nil { + cfg.Next = ConfigDefault.Next + } + if cfg.Level < -1 || cfg.Level > 2 { + cfg.Level = ConfigDefault.Level + } + } + + // Setup request handlers + var ( + fctx = func(c *fasthttp.RequestCtx) {} + compressor fasthttp.RequestHandler + ) + + // Setup compression algorithm + switch cfg.Level { + case 0: + // LevelDefault + compressor = fasthttp.CompressHandlerBrotliLevel(fctx, fasthttp.CompressBrotliDefaultCompression, fasthttp.CompressDefaultCompression) + case 1: + // LevelBestSpeed + compressor = fasthttp.CompressHandlerBrotliLevel(fctx, fasthttp.CompressBrotliBestSpeed, fasthttp.CompressBestSpeed) + case 2: + // LevelBestCompression + compressor = fasthttp.CompressHandlerBrotliLevel(fctx, fasthttp.CompressBrotliBestCompression, fasthttp.CompressBestCompression) + default: + // LevelDisabled + return func(c *fiber.Ctx) error { + return c.Next() + } + } + + // Return new handler + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Continue stack + if err := c.Next(); err != nil { + return err + } + + // Compress response + compressor(c.Context()) + + // Return from handler + return nil + } +} diff --git a/middleware/compress/compress_test.go b/middleware/compress/compress_test.go new file mode 100644 index 00000000..66aa64f5 --- /dev/null +++ b/middleware/compress/compress_test.go @@ -0,0 +1,97 @@ +package compress + +import ( + "io/ioutil" + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +var filedata []byte + +func init() { + dat, err := ioutil.ReadFile("../../.github/README.md") + if err != nil { + panic(err) + } + filedata = dat +} + +// go test -run Test_Compress +func Test_Compress_Gzip(t *testing.T) { + app := fiber.New() + + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) + return c.Send(filedata) + }) + + req := httptest.NewRequest("GET", "/", nil) + req.Header.Set("Accept-Encoding", "gzip") + + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, "gzip", resp.Header.Get(fiber.HeaderContentEncoding)) + + // Validate the file size is shrinked + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + utils.AssertEqual(t, nil, err) + } + utils.AssertEqual(t, true, len(body) < len(filedata)) +} + +func Test_Compress_Deflate(t *testing.T) { + app := fiber.New() + + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + return c.Send(filedata) + }) + + req := httptest.NewRequest("GET", "/", nil) + req.Header.Set("Accept-Encoding", "deflate") + + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, "deflate", resp.Header.Get(fiber.HeaderContentEncoding)) + + // Validate the file size is shrinked + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + utils.AssertEqual(t, nil, err) + } + utils.AssertEqual(t, true, len(body) < len(filedata)) +} + +func Test_Compress_Brotli(t *testing.T) { + app := fiber.New() + + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + return c.Send(filedata) + }) + + req := httptest.NewRequest("GET", "/", nil) + req.Header.Set("Accept-Encoding", "br") + + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, "br", resp.Header.Get(fiber.HeaderContentEncoding)) + + // Validate the file size is shrinked + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + utils.AssertEqual(t, nil, err) + } + utils.AssertEqual(t, true, len(body) < len(filedata)) +} diff --git a/middleware/compress_test.go b/middleware/compress_test.go deleted file mode 100644 index 83ac58f6..00000000 --- a/middleware/compress_test.go +++ /dev/null @@ -1,172 +0,0 @@ -package middleware - -import ( - "fmt" - "net/http/httptest" - "os" - "strconv" - "testing" - - "github.com/gofiber/fiber" - "github.com/gofiber/utils" - "github.com/valyala/fasthttp" -) - -// go test -run Test_Middleware_Compress -func Test_Middleware_Compress(t *testing.T) { - app := fiber.New() - - app.Use(Compress()) - - app.Get("/", func(c *fiber.Ctx) { - c.SendFile(compressFilePath(CompressLevelDefault), true) - }) - - req := httptest.NewRequest("GET", "/", nil) - req.Header.Set(fiber.HeaderAcceptEncoding, "gzip") - - resp, err := app.Test(req) - utils.AssertEqual(t, nil, err, "app.Test(req)") - utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") - utils.AssertEqual(t, "gzip", resp.Header.Get(fiber.HeaderContentEncoding)) - utils.AssertEqual(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) - os.Remove(compressFilePath(CompressLevelDefault, true)) -} - -// go test -run Test_Middleware_Compress_Config -func Test_Middleware_Compress_Config(t *testing.T) { - app := fiber.New() - - app.Use(Compress(CompressConfig{ - Level: CompressLevelDefault, - })) - - app.Get("/", func(c *fiber.Ctx) { - c.SendFile(compressFilePath(CompressLevelDefault), true) - }) - - req := httptest.NewRequest("GET", "/", nil) - req.Header.Set(fiber.HeaderAcceptEncoding, "gzip") - - resp, err := app.Test(req) - utils.AssertEqual(t, nil, err, "app.Test(req)") - utils.AssertEqual(t, 200, resp.StatusCode, "Status code") - utils.AssertEqual(t, "gzip", resp.Header.Get(fiber.HeaderContentEncoding)) - utils.AssertEqual(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) - os.Remove(compressFilePath(CompressLevelDefault, true)) -} - -// go test -run Test_Middleware_Compress_With_Config -func Test_Middleware_Compress_With_Config(t *testing.T) { - app := fiber.New() - - app.Use(Compress(CompressConfig{})) - - app.Get("/", func(c *fiber.Ctx) { - c.SendFile(compressFilePath(CompressLevelDefault), true) - }) - - req := httptest.NewRequest("GET", "/", nil) - req.Header.Set(fiber.HeaderAcceptEncoding, "gzip") - - resp, err := app.Test(req) - utils.AssertEqual(t, nil, err, "app.Test(req)") - utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") - utils.AssertEqual(t, "gzip", resp.Header.Get(fiber.HeaderContentEncoding)) - utils.AssertEqual(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) - os.Remove(compressFilePath(CompressLevelDefault, true)) -} - -// go test -run Test_Middleware_Compress_Level -func Test_Middleware_Compress_Level(t *testing.T) { - t.Parallel() - - levels := []int{ - CompressLevelDisabled, - CompressLevelDefault, - CompressLevelBestSpeed, - CompressLevelBestCompression, - } - - app := fiber.New() - for _, level := range levels { - app.Get("/:level", Compress(level), func(c *fiber.Ctx) { - c.SendFile(compressFilePath(c.Params("level")), true) - }) - } - - for _, level := range levels { - name := strconv.FormatInt(int64(level), 10) - t.Run(name, func(t *testing.T) { - target := fmt.Sprintf("/%d", level) - req := httptest.NewRequest("GET", target, nil) - req.Header.Set(fiber.HeaderAcceptEncoding, "br") - - resp, err := app.Test(req, 3000) - utils.AssertEqual(t, nil, err, "app.Test(req)") - utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") - utils.AssertEqual(t, "br", resp.Header.Get(fiber.HeaderContentEncoding)) - utils.AssertEqual(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) - - os.Remove(compressFilePath(level, true)) - }) - } -} - -// go test -run Test_Middleware_Compress_Skip -func Test_Middleware_Compress_Skip(t *testing.T) { - app := fiber.New() - - app.Use(Compress(func(c *fiber.Ctx) bool { return true })) - - app.Get("/", func(c *fiber.Ctx) { - c.SendFile(compressFilePath(CompressLevelDefault), true) - }) - - req := httptest.NewRequest("GET", "/", nil) - req.Header.Set(fiber.HeaderAcceptEncoding, "br") - - resp, err := app.Test(req, 3000) - utils.AssertEqual(t, nil, err, "app.Test(req)") - utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") - utils.AssertEqual(t, "", resp.Header.Get(fiber.HeaderContentEncoding)) - utils.AssertEqual(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) -} - -// go test -run Test_Middleware_Compress_Panic -func Test_Middleware_Compress_Panic(t *testing.T) { - defer func() { - utils.AssertEqual(t, - "Compress: the following option types are allowed: int, func(*fiber.Ctx) bool, CompressConfig", - fmt.Sprintf("%s", recover())) - }() - - Compress("invalid") -} - -// go test -v ./... -run=^$ -bench=Benchmark_Middleware_Compress -benchmem -count=4 -func Benchmark_Middleware_Compress(b *testing.B) { - app := fiber.New() - app.Use(Compress()) - app.Get("/", func(c *fiber.Ctx) { - c.SendFile(compressFilePath(CompressLevelDefault), true) - }) - handler := app.Handler() - - c := &fasthttp.RequestCtx{} - c.Request.SetRequestURI("/") - - b.ReportAllocs() - b.ResetTimer() - for n := 0; n < b.N; n++ { - handler(c) - } -} - -func compressFilePath(level interface{}, gz ...bool) string { - filePath := fmt.Sprintf("./testdata/compress_level_%v.txt", level) - if len(gz) > 0 && gz[0] { - filePath += ".fiber.gz" - } - return filePath -} diff --git a/middleware/cors/README.md b/middleware/cors/README.md new file mode 100644 index 00000000..6fa3bb47 --- /dev/null +++ b/middleware/cors/README.md @@ -0,0 +1,95 @@ +# Cross-Origin Resource Sharing (CORS) +CORS middleware for [Fiber](https://github.com/gofiber/fiber) that that can be used to enable [Cross-Origin Resource Sharing](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) with various options. + +### Table of Contents +- [Signatures](#signatures) +- [Examples](#examples) +- [Config](#config) +- [Default Config](#default-config) + +### Signatures +```go +func New(config ...Config) fiber.Handler +``` + +### Examples +Import the middleware package that is part of the Fiber web framework +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: +```go +// Default config +app.Use(cors.New()) + +// Or extend your config for customization +app.Use(cors.New(cors.Config{ + AllowOrigins: "https://gofiber.io, https://gofiber.net", + AllowHeader: "Origin, Content-Type, Accept", +})) +``` + +### Config +```go +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // AllowOrigin defines a list of origins that may access the resource. + // + // Optional. Default value "*" + AllowOrigins string + + // AllowMethods defines a list methods allowed when accessing the resource. + // This is used in response to a preflight request. + // + // Optional. Default value "GET,POST,HEAD,PUT,DELETE,PATCH" + AllowMethods string + + // AllowHeaders defines a list of request headers that can be used when + // making the actual request. This is in response to a preflight request. + // + // Optional. Default value "". + AllowHeaders string + + // AllowCredentials indicates whether or not the response to the request + // can be exposed when the credentials flag is true. When used as part of + // a response to a preflight request, this indicates whether or not the + // actual request can be made using credentials. + // + // Optional. Default value false. + AllowCredentials bool + + // ExposeHeaders defines a whitelist headers that clients are allowed to + // access. + // + // Optional. Default value "". + ExposeHeaders string + + // MaxAge indicates how long (in seconds) the results of a preflight request + // can be cached. + // + // Optional. Default value 0. + MaxAge int +} +``` + +### Default Config +```go +var ConfigDefault = Config{ + Next: nil, + AllowOrigins: "*", + AllowMethods: "GET,POST,HEAD,PUT,DELETE,PATCH", + AllowHeaders: "", + AllowCredentials: false, + ExposeHeaders: "", + MaxAge: 0, +} +``` \ No newline at end of file diff --git a/middleware/cors/cors.go b/middleware/cors/cors.go new file mode 100644 index 00000000..cb69fd55 --- /dev/null +++ b/middleware/cors/cors.go @@ -0,0 +1,177 @@ +package cors + +import ( + "net/http" + "strconv" + "strings" + + "github.com/gofiber/fiber/v2" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // AllowOrigin defines a list of origins that may access the resource. + // + // Optional. Default value "*" + AllowOrigins string + + // AllowMethods defines a list methods allowed when accessing the resource. + // This is used in response to a preflight request. + // + // Optional. Default value "GET,POST,HEAD,PUT,DELETE,PATCH" + AllowMethods string + + // AllowHeaders defines a list of request headers that can be used when + // making the actual request. This is in response to a preflight request. + // + // Optional. Default value "". + AllowHeaders string + + // AllowCredentials indicates whether or not the response to the request + // can be exposed when the credentials flag is true. When used as part of + // a response to a preflight request, this indicates whether or not the + // actual request can be made using credentials. + // + // Optional. Default value false. + AllowCredentials bool + + // ExposeHeaders defines a whitelist headers that clients are allowed to + // access. + // + // Optional. Default value "". + ExposeHeaders string + + // MaxAge indicates how long (in seconds) the results of a preflight request + // can be cached. + // + // Optional. Default value 0. + MaxAge int +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Next: nil, + AllowOrigins: "*", + AllowMethods: strings.Join([]string{ + fiber.MethodGet, + fiber.MethodPost, + fiber.MethodHead, + fiber.MethodPut, + fiber.MethodDelete, + fiber.MethodPatch, + }, ","), + AllowHeaders: "", + AllowCredentials: false, + ExposeHeaders: "", + MaxAge: 0, +} + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + // Set default config + cfg := ConfigDefault + + // Override config if provided + if len(config) > 0 { + cfg = config[0] + + // Set default values + if cfg.Next == nil { + cfg.Next = ConfigDefault.Next + } + if cfg.AllowOrigins == "" { + cfg.AllowOrigins = ConfigDefault.AllowOrigins + } + if cfg.AllowMethods == "" { + cfg.AllowMethods = ConfigDefault.AllowMethods + } + } + + // Convert string to slice + allowOrigins := strings.Split(strings.Replace(cfg.AllowOrigins, " ", "", -1), ",") + + // Strip white spaces + allowMethods := strings.Replace(cfg.AllowMethods, " ", "", -1) + allowHeaders := strings.Replace(cfg.AllowHeaders, " ", "", -1) + exposeHeaders := strings.Replace(cfg.ExposeHeaders, " ", "", -1) + + // Convert int to string + maxAge := strconv.Itoa(cfg.MaxAge) + + // Return new handler + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Get origin header + origin := c.Get(fiber.HeaderOrigin) + allowOrigin := "" + + // Check allowed origins + for _, o := range allowOrigins { + if o == "*" && cfg.AllowCredentials { + allowOrigin = origin + break + } + if o == "*" || o == origin { + allowOrigin = o + break + } + if matchSubdomain(origin, o) { + allowOrigin = origin + break + } + } + + // Simple request + if c.Method() != http.MethodOptions { + c.Vary(fiber.HeaderOrigin) + c.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin) + + if cfg.AllowCredentials { + c.Set(fiber.HeaderAccessControlAllowCredentials, "true") + } + if exposeHeaders != "" { + c.Set(fiber.HeaderAccessControlExposeHeaders, exposeHeaders) + } + return c.Next() + } + + // Preflight request + c.Vary(fiber.HeaderOrigin) + c.Vary(fiber.HeaderAccessControlRequestMethod) + c.Vary(fiber.HeaderAccessControlRequestHeaders) + c.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin) + c.Set(fiber.HeaderAccessControlAllowMethods, allowMethods) + + // Set Allow-Credentials if set to true + if cfg.AllowCredentials { + c.Set(fiber.HeaderAccessControlAllowCredentials, "true") + } + + // Set Allow-Headers if not empty + if allowHeaders != "" { + c.Set(fiber.HeaderAccessControlAllowHeaders, allowHeaders) + } else { + h := c.Get(fiber.HeaderAccessControlRequestHeaders) + if h != "" { + c.Set(fiber.HeaderAccessControlAllowHeaders, h) + } + } + + // Set MaxAge is set + if cfg.MaxAge > 0 { + c.Set(fiber.HeaderAccessControlMaxAge, maxAge) + } + + // Send 204 No Content + return c.SendStatus(fiber.StatusNoContent) + } +} diff --git a/middleware/cors/cors_test.go b/middleware/cors/cors_test.go new file mode 100644 index 00000000..1f7081b1 --- /dev/null +++ b/middleware/cors/cors_test.go @@ -0,0 +1,195 @@ +package cors + +import ( + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" + "github.com/valyala/fasthttp" +) + +func Test_CORS_Defaults(t *testing.T) { + app := fiber.New() + app.Use(New()) + + h := app.Handler() + + // Test default GET response headers + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + h(ctx) + + utils.AssertEqual(t, "*", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowCredentials))) + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlExposeHeaders))) + + // Test default OPTIONS (preflight) response headers + ctx = &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodOptions) + h(ctx) + + utils.AssertEqual(t, "GET,POST,HEAD,PUT,DELETE,PATCH", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowMethods))) + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowHeaders))) + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlMaxAge))) +} + +// go test -run -v Test_CORS_Wildcard +func Test_CORS_Wildcard(t *testing.T) { + // New fiber instance + app := fiber.New() + // Get handler pointer + handler := app.Handler() + + // OPTIONS (preflight) response headers when AllowOrigins is * + app.Use(New(Config{AllowOrigins: "*", AllowCredentials: true, MaxAge: 3600})) + + // Make request + ctx := &fasthttp.RequestCtx{} + ctx.Request.SetRequestURI("/") + ctx.Request.Header.Set(fiber.HeaderOrigin, "localhost") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + + // Perform request + handler(ctx) + + // Check result + utils.AssertEqual(t, "localhost", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) + utils.AssertEqual(t, "true", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowCredentials))) + utils.AssertEqual(t, "3600", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlMaxAge))) +} + +// go test -run -v Test_CORS_Subdomain +func Test_CORS_Subdomain(t *testing.T) { + // New fiber instance + app := fiber.New() + // Get handler pointer + handler := app.Handler() + + // OPTIONS (preflight) response headers when AllowOrigins is set to a subdomain + app.Use("/", New(Config{AllowOrigins: "http://*.example.com"})) + + // Make request with disallowed origin + ctx := &fasthttp.RequestCtx{} + ctx.Request.SetRequestURI("/") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://google.com") + + // Perform request + handler(ctx) + + // Allow-Origin header should be "" because http://google.com does not satisfy http://*.example.com + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) + + ctx.Request.Reset() + ctx.Response.Reset() + + // Make request with allowed origin + ctx.Request.SetRequestURI("/") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://test.example.com") + + handler(ctx) + + utils.AssertEqual(t, "http://test.example.com", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) +} + +func Test_CORS_AllowOriginScheme(t *testing.T) { + tests := []struct { + reqOrigin, pattern string + shouldAllowOrigin bool + }{ + { + pattern: "http://example.com", + reqOrigin: "http://example.com", + shouldAllowOrigin: true, + }, + { + pattern: "https://example.com", + reqOrigin: "https://example.com", + shouldAllowOrigin: true, + }, + { + pattern: "http://example.com", + reqOrigin: "https://example.com", + shouldAllowOrigin: false, + }, + { + pattern: "http://*.example.com", + reqOrigin: "http://aaa.example.com", + shouldAllowOrigin: true, + }, + { + pattern: "http://*.example.com", + reqOrigin: "http://bbb.aaa.example.com", + shouldAllowOrigin: true, + }, + { + pattern: "http://*.aaa.example.com", + reqOrigin: "http://bbb.aaa.example.com", + shouldAllowOrigin: true, + }, + { + pattern: "http://*.example.com:8080", + reqOrigin: "http://aaa.example.com:8080", + shouldAllowOrigin: true, + }, + { + pattern: "http://example.com", + reqOrigin: "http://gofiber.com", + shouldAllowOrigin: false, + }, + { + pattern: "http://*.aaa.example.com", + reqOrigin: "http://ccc.bbb.example.com", + shouldAllowOrigin: false, + }, + { + pattern: "http://*.example.com", + reqOrigin: `http://1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890\ + .1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890\ + .1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890\ + .1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.example.com`, + shouldAllowOrigin: false, + }, + { + pattern: "http://example.com", + reqOrigin: "http://ccc.bbb.example.com", + shouldAllowOrigin: false, + }, + { + pattern: "https://*--aaa.bbb.com", + reqOrigin: "https://prod-preview--aaa.bbb.com", + shouldAllowOrigin: false, + }, + { + pattern: "http://*.example.com", + reqOrigin: "http://ccc.bbb.example.com", + shouldAllowOrigin: true, + }, + { + pattern: "http://foo.[a-z]*.example.com", + reqOrigin: "http://ccc.bbb.example.com", + shouldAllowOrigin: false, + }, + } + + for _, tt := range tests { + app := fiber.New() + app.Use("/", New(Config{AllowOrigins: tt.pattern})) + + handler := app.Handler() + + ctx := &fasthttp.RequestCtx{} + ctx.Request.SetRequestURI("/") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderOrigin, tt.reqOrigin) + + handler(ctx) + + if tt.shouldAllowOrigin { + utils.AssertEqual(t, tt.reqOrigin, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) + } else { + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) + } + } +} diff --git a/middleware/cors/utils.go b/middleware/cors/utils.go new file mode 100644 index 00000000..fee658ef --- /dev/null +++ b/middleware/cors/utils.go @@ -0,0 +1,52 @@ +package cors + +import "strings" + +func matchScheme(domain, pattern string) bool { + didx := strings.Index(domain, ":") + pidx := strings.Index(pattern, ":") + return didx != -1 && pidx != -1 && domain[:didx] == pattern[:pidx] +} + +// matchSubdomain compares authority with wildcard +func matchSubdomain(domain, pattern string) bool { + if !matchScheme(domain, pattern) { + return false + } + didx := strings.Index(domain, "://") + pidx := strings.Index(pattern, "://") + if didx == -1 || pidx == -1 { + return false + } + domAuth := domain[didx+3:] + // to avoid long loop by invalid long domain + if len(domAuth) > 253 { + return false + } + patAuth := pattern[pidx+3:] + + domComp := strings.Split(domAuth, ".") + patComp := strings.Split(patAuth, ".") + for i := len(domComp)/2 - 1; i >= 0; i-- { + opp := len(domComp) - 1 - i + domComp[i], domComp[opp] = domComp[opp], domComp[i] + } + for i := len(patComp)/2 - 1; i >= 0; i-- { + opp := len(patComp) - 1 - i + patComp[i], patComp[opp] = patComp[opp], patComp[i] + } + + for i, v := range domComp { + if len(patComp) <= i { + return false + } + p := patComp[i] + if p == "*" { + return true + } + if p != v { + return false + } + } + return false +} diff --git a/middleware/csrf/csrf.go b/middleware/csrf/csrf.go new file mode 100644 index 00000000..805cf918 --- /dev/null +++ b/middleware/csrf/csrf.go @@ -0,0 +1,203 @@ +package csrf + +import ( + "crypto/subtle" + "errors" + "strings" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // TokenLookup is a string in the form of ":" that is used + // to extract token from the request. + // + // Optional. Default value "header:X-CSRF-Token". + // Possible values: + // - "header:" + // - "query:" + // - "param:" + // - "form:" + TokenLookup string + + // Cookie + // + // Optional. + Cookie *fiber.Cookie + + // CookieExpires + // + // Optional. Default: time.Now().Add(24 * time.Hour) + CookieExpires time.Time + + // Context key to store generated CSRF token into context. + // + // Optional. Default value "csrf". + ContextKey string +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Next: nil, + TokenLookup: "header:X-CSRF-Token", + ContextKey: "csrf", + Cookie: &fiber.Cookie{ + Name: "_csrf", + Domain: "", + Path: "", + Secure: false, + HTTPOnly: false, + }, +} + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + // Set default config + cfg := ConfigDefault + + // Override config if provided + if len(config) > 0 { + cfg = config[0] + + // Set default values + if cfg.TokenLookup == "" { + cfg.TokenLookup = ConfigDefault.TokenLookup + } + if cfg.ContextKey == "" { + cfg.ContextKey = ConfigDefault.ContextKey + } + if cfg.Cookie == nil { + cfg.Cookie = ConfigDefault.Cookie + } + if cfg.CookieExpires.IsZero() { + cfg.CookieExpires = ConfigDefault.CookieExpires + } + } + + // Generate the correct extractor to get the token from the correct location + selectors := strings.Split(cfg.TokenLookup, ":") + + // By default we extract from a header + extractor := csrfFromHeader(selectors[1]) + + switch selectors[0] { + case "form": + extractor = csrfFromForm(selectors[1]) + case "query": + extractor = csrfFromQuery(selectors[1]) + case "param": + extractor = csrfFromParam(selectors[1]) + } + + // Return new handler + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Declare empty token and try to get previous generated CSRF from cookie + token, key := "", c.Cookies(cfg.Cookie.Name) + + // Check if the cookie had a CSRF token + if key == "" { + // Create a new CSRF token + token = utils.UUID() + } else { + // Use the server generated token previously to compare + // To the extracted token later on + token = key + } + + // Verify CSRF token on POST requests + if c.Method() == fiber.MethodPost { + // Extract token from client request i.e. header, query, param or form + csrf, err := extractor(c) + if err != nil { + // We have a problem extracting the csrf token + return c.SendStatus(fiber.StatusBadRequest) + } + // Some magic to compare both cookie and client csrf token + if subtle.ConstantTimeCompare(utils.GetBytes(token), utils.GetBytes(csrf)) != 1 { + // Comparison failed, return forbidden + return c.SendStatus(fiber.StatusForbidden) + } + } + + // Create new cookie to send new CSRF token + cookie := &fiber.Cookie{ + Name: cfg.Cookie.Name, + Value: token, + Domain: cfg.Cookie.Domain, + Path: cfg.Cookie.Path, + Expires: cfg.CookieExpires, + Secure: cfg.Cookie.Secure, + HTTPOnly: cfg.Cookie.HTTPOnly, + } + + // Set cookie to response + c.Cookie(cookie) + + // Store token in context + c.Locals(cfg.ContextKey, token) + + // Protect clients from caching the response by telling the browser + // a new header value is generated + c.Vary(fiber.HeaderCookie) + + // Continue stack + return c.Next() + } +} + +// csrfFromHeader returns a function that extracts token from the request header. +func csrfFromHeader(param string) func(c *fiber.Ctx) (string, error) { + return func(c *fiber.Ctx) (string, error) { + token := c.Get(param) + if token == "" { + return "", errors.New("missing csrf token in header") + } + return token, nil + } +} + +// csrfcsrfFromQuery returns a function that extracts token from the query string. +func csrfFromQuery(param string) func(c *fiber.Ctx) (string, error) { + return func(c *fiber.Ctx) (string, error) { + token := c.Query(param) + if token == "" { + return "", errors.New("missing csrf token in query string") + } + return token, nil + } +} + +// csrfFromParam returns a function that extracts token from the url param string. +func csrfFromParam(param string) func(c *fiber.Ctx) (string, error) { + return func(c *fiber.Ctx) (string, error) { + token := c.Params(param) + if token == "" { + return "", errors.New("missing csrf token in url parameter") + } + return token, nil + } +} + +// csrfFromParam returns a function that extracts token from the url param string. +func csrfFromForm(param string) func(c *fiber.Ctx) (string, error) { + return func(c *fiber.Ctx) (string, error) { + token := c.FormValue(param) + if token == "" { + return "", errors.New("missing csrf token in form parameter") + } + return token, nil + } +} diff --git a/middleware/csrf/csrf_test.go b/middleware/csrf/csrf_test.go new file mode 100644 index 00000000..a556ab80 --- /dev/null +++ b/middleware/csrf/csrf_test.go @@ -0,0 +1 @@ +package csrf diff --git a/middleware/favicon.go b/middleware/favicon.go deleted file mode 100644 index ca64ed12..00000000 --- a/middleware/favicon.go +++ /dev/null @@ -1,50 +0,0 @@ -package middleware - -import ( - "io/ioutil" - "strconv" - - "github.com/gofiber/fiber" -) - -// Favicon adds an UUID indentifier to the request -func Favicon(file ...string) fiber.Handler { - var err error - var icon []byte - - // Set lookup if provided - if len(file) > 0 { - icon, err = ioutil.ReadFile(file[0]) - if err != nil { - panic(err) - } - } - // Return handler - return func(c *fiber.Ctx) { - if len(c.Path()) != 12 || c.Path() != "/favicon.ico" { - c.Next() - return - } - - if c.Method() != fiber.MethodGet && c.Method() != fiber.MethodHead { - if c.Method() != fiber.MethodOptions { - c.Status(405) - } else { - c.Status(200) - } - c.Set(fiber.HeaderAllow, "GET, HEAD, OPTIONS") - c.Set(fiber.HeaderContentLength, "0") - return - } - - if len(icon) > 0 { - c.Set(fiber.HeaderContentLength, strconv.Itoa(len(icon))) - c.Set(fiber.HeaderContentType, "image/x-icon") - c.Set(fiber.HeaderCacheControl, "public, max-age=31536000") - c.Status(200).SendBytes(icon) - return - } - - c.Status(204) - } -} diff --git a/middleware/favicon.md b/middleware/favicon.md deleted file mode 100644 index 4d939ccd..00000000 --- a/middleware/favicon.md +++ /dev/null @@ -1,33 +0,0 @@ -# Favicon -Favicon middleware ignores favicon requests or caches a provided icon in memory to improve performance by skipping disk access. User agents request favicon.ico frequently and indiscriminately, so you may wish to exclude these requests from your logs by using this middleware before your logger middleware. - -**Note** This middleware is exclusively for serving the _default, implicit favicon_, which is `GET /favicon.ico`. - -### Example -Import the middleware package that is part of the Fiber web framework -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) -``` - -After you initiate your Fiber app, you can use the following possibilities: -```go -func main() { - app := fiber.New() - - // Default ignore favicon - app.Use(middleware.Favicon()) - - // Pass a favicon file that will be cached in memory - app.Use(middleware.Favicon("./favicon.ico")) - - // ... -} -``` - -### Signatures -```go -func Favicon(file ...string) fiber.Handler {} -``` \ No newline at end of file diff --git a/middleware/favicon/README.md b/middleware/favicon/README.md new file mode 100644 index 00000000..c31ed2b5 --- /dev/null +++ b/middleware/favicon/README.md @@ -0,0 +1,60 @@ +# Favicon Authentication +Favicon middleware for [Fiber](https://github.com/gofiber/fiber) that ignores favicon requests or caches a provided icon in memory to improve performance by skipping disk access. User agents request favicon.ico frequently and indiscriminately, so you may wish to exclude these requests from your logs by using this middleware before your logger middleware. + +**Note** This middleware is exclusively for serving the default, implicit favicon, which is GET /favicon.ico. + +### Table of Contents +- [Signatures](#signatures) +- [Examples](#examples) +- [Config](#config) +- [Default Config](#default-config) + + +### Signatures +```go +func New(config ...Config) fiber.Handler +``` + +### Examples +Import the middleware package that is part of the Fiber web framework +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/favicon" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: +```go +// Provide a minimal config +app.Use(favicon.New()) + +// Or extend your config for customization +app.Use(favicon.New(favicon.Config{ + File: "./favicon.ico" +})) +``` + +### Config +```go +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // File holds the path to an actual favicon that will be cached + // + // Optional. Default: "" + File string +} +``` + +### Default Config +```go +var ConfigDefault = Config{ + Next: nil, + File: "" +} +``` diff --git a/middleware/favicon/favicon.go b/middleware/favicon/favicon.go new file mode 100644 index 00000000..b4ae1e2d --- /dev/null +++ b/middleware/favicon/favicon.go @@ -0,0 +1,101 @@ +package favicon + +import ( + "io/ioutil" + "strconv" + + "github.com/gofiber/fiber/v2" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // File holds the path to an actual favicon that will be cached + // + // Optional. Default: "" + File string +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Next: nil, + File: "", +} + +const ( + hType = "image/x-icon" + hAllow = "GET, HEAD, OPTIONS" + hCache = "public, max-age=31536000" + hZero = "0" +) + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + // Set default config + cfg := ConfigDefault + + // Override config if provided + if len(config) > 0 { + cfg = config[0] + + // Set default values + if cfg.Next == nil { + cfg.Next = ConfigDefault.Next + } + if cfg.File == "" { + cfg.File = ConfigDefault.File + } + } + + // Load icon if provided + var ( + err error + icon []byte + iconLen string + ) + if cfg.File != "" { + if icon, err = ioutil.ReadFile(cfg.File); err != nil { + panic(err) + } + iconLen = strconv.Itoa(len(icon)) + } + + // Return new handler + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Only respond to favicon requests + if len(c.Path()) != 12 || c.Path() != "/favicon.ico" { + return c.Next() + } + + // Only allow GET, HEAD and OPTIONS requests + if c.Method() != fiber.MethodGet && c.Method() != fiber.MethodHead { + if c.Method() != fiber.MethodOptions { + c.Status(fiber.StatusMethodNotAllowed) + } else { + c.Status(fiber.StatusOK) + } + c.Set(fiber.HeaderAllow, hAllow) + c.Set(fiber.HeaderContentLength, hZero) + return nil + } + + // Serve cached favicon + if len(icon) > 0 { + c.Set(fiber.HeaderContentLength, iconLen) + c.Set(fiber.HeaderContentType, hType) + c.Set(fiber.HeaderCacheControl, hCache) + return c.Status(fiber.StatusOK).Send(icon) + } + + return c.SendStatus(fiber.StatusNoContent) + } +} diff --git a/middleware/favicon_test.go b/middleware/favicon/favicon_test.go similarity index 82% rename from middleware/favicon_test.go rename to middleware/favicon/favicon_test.go index 85021e14..78cf63a4 100644 --- a/middleware/favicon_test.go +++ b/middleware/favicon/favicon_test.go @@ -1,11 +1,11 @@ -package middleware +package favicon import ( "net/http/httptest" "testing" - "github.com/gofiber/fiber" - "github.com/gofiber/utils" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" "github.com/valyala/fasthttp" ) @@ -13,9 +13,11 @@ import ( func Test_Middleware_Favicon(t *testing.T) { app := fiber.New() - app.Use(Favicon()) + app.Use(New()) - app.Get("/", func(c *fiber.Ctx) {}) + app.Get("/", func(c *fiber.Ctx) error { + return nil + }) // Skip Favicon middleware resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) @@ -44,16 +46,22 @@ func Test_Middleware_Favicon_Not_Found(t *testing.T) { } }() - fiber.New().Use(Favicon("non-exist.ico")) + fiber.New().Use(New(Config{ + File: "non-exist.ico", + })) } // go test -run Test_Middleware_Favicon_Found func Test_Middleware_Favicon_Found(t *testing.T) { app := fiber.New() - app.Use(Favicon("./testdata/favicon.ico")) + app.Use(New(Config{ + File: "../../.github/testdata/favicon.ico", + })) - app.Get("/", func(c *fiber.Ctx) {}) + app.Get("/", func(c *fiber.Ctx) error { + return nil + }) resp, err := app.Test(httptest.NewRequest("GET", "/favicon.ico", nil)) @@ -65,8 +73,10 @@ func Test_Middleware_Favicon_Found(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Middleware_Favicon -benchmem -count=4 func Benchmark_Middleware_Favicon(b *testing.B) { app := fiber.New() - app.Use(Favicon()) - app.Get("/", func(c *fiber.Ctx) {}) + app.Use(New()) + app.Get("/", func(c *fiber.Ctx) error { + return nil + }) handler := app.Handler() c := &fasthttp.RequestCtx{} diff --git a/middleware/filesystem.go b/middleware/filesystem.go deleted file mode 100644 index 372b507b..00000000 --- a/middleware/filesystem.go +++ /dev/null @@ -1,221 +0,0 @@ -package middleware - -import ( - "fmt" - "html" - "log" - "net/http" - "os" - "path" - "sort" - "strings" - - fiber "github.com/gofiber/fiber" -) - -// Middleware types -type ( - // FileSystemConfig defines the config for FileSystem middleware. - FileSystemConfig struct { - // Next defines a function to skip this middleware if returned true. - Next func(ctx *fiber.Ctx) bool - - // Root is a FileSystem that provides access - // to a collection of files and directories. - // Required. Default: nil - Root http.FileSystem - - // Index file for serving a directory. - // Optional. Default: "index.html" - Index string - - // Enable directory browsing. - // Optional. Default: false - Browse bool - } -) - -// FileSystemConfigDefault is the default config -var FileSystemConfigDefault = FileSystemConfig{ - Next: nil, - Root: nil, - Index: "/index.html", - Browse: false, -} - -/* -FileSystem allows the following config arguments in any order: - - FileSystem(root http.FileSystem) - - FileSystem(index string) - - FileSystem(browse bool) - - FileSystem(config FileSystem) - - FileSystem(next func(*fiber.Ctx) bool) -*/ -func FileSystem(options ...interface{}) fiber.Handler { - // Create default config - var config = FileSystemConfigDefault - // Assert options if provided to adjust the config - for i := range options { - switch opt := options[i].(type) { - case func(*fiber.Ctx) bool: - config.Next = opt - case http.FileSystem: - config.Root = opt - case string: - config.Index = opt - case bool: - config.Browse = opt - case FileSystemConfig: - config = opt - default: - log.Fatal("FileSystem: the following option types are allowed: string, bool, http.FileSystem, FileSystemConfig") - } - } - // Return fileSystem - return fileSystem(config) -} - -func fileSystem(config FileSystemConfig) fiber.Handler { - // Set config default values - if config.Index == "" { - config.Index = FileSystemConfigDefault.Index - } - if !strings.HasPrefix(config.Index, "/") { - config.Index = "/" + config.Index - } - if config.Root == nil { - log.Fatal("FileSystem: Root value is missing!") - } - - // Middleware settings - var prefix string - - // Return handler - return func(c *fiber.Ctx) { - // Set prefix - if len(prefix) == 0 { - prefix = c.Route().Path - } - - // Strip prefix - path := strings.TrimPrefix(c.Path(), prefix) - if !strings.HasPrefix(path, "/") { - path = "/" + path - } - - file, err := config.Root.Open(path) - if err != nil { - c.Next(err) - return - } - - stat, err := file.Stat() - if err != nil { - c.Next(err) - return - } - - // Serve index if path is directory - if stat.IsDir() { - indexPath := strings.TrimSuffix(path, "/") + config.Index - index, err := config.Root.Open(indexPath) - if err == nil { - indexStat, err := index.Stat() - if err == nil { - file = index - stat = indexStat - } - } - } - - // Browse directory if no index found and browsing is enabled - if stat.IsDir() { - if config.Browse { - if err := dirList(c, file); err != nil { - c.Next(err) - } - return - } - c.SendStatus(fiber.StatusForbidden) - return - } - - modTime := stat.ModTime() - contentLength := int(stat.Size()) - - // Set Content Type header - c.Type(getFileExtension(stat.Name())) - - // Set Last Modified header - if !modTime.IsZero() { - c.Set(fiber.HeaderLastModified, modTime.UTC().Format(http.TimeFormat)) - } - - if c.Method() == fiber.MethodGet { - c.Fasthttp.SetBodyStream(file, contentLength) - return - } else if c.Method() == fiber.MethodHead { - c.Fasthttp.ResetBody() - c.Fasthttp.Response.SkipBody = true - c.Fasthttp.Response.Header.SetContentLength(contentLength) - if err := file.Close(); err != nil { - c.Next(err) - } - return - } - - c.Next() - } -} - -func getFileExtension(path string) string { - n := strings.LastIndexByte(path, '.') - if n < 0 { - return "" - } - return path[n:] -} - -func dirList(c *fiber.Ctx, f http.File) error { - fileinfos, err := f.Readdir(-1) - if err != nil { - return err - } - - fm := make(map[string]os.FileInfo, len(fileinfos)) - filenames := make([]string, 0, len(fileinfos)) - for _, fi := range fileinfos { - name := fi.Name() - fm[name] = fi - filenames = append(filenames, name) - } - - basePathEscaped := html.EscapeString(c.Path()) - c.Write(fmt.Sprintf("%s", basePathEscaped)) - c.Write(fmt.Sprintf("

%s

", basePathEscaped)) - c.Write("
    ") - - if len(basePathEscaped) > 1 { - parentPathEscaped := html.EscapeString(c.Path() + "/..") - c.Write(fmt.Sprintf(`
  • ..
  • `, parentPathEscaped)) - } - - sort.Strings(filenames) - for _, name := range filenames { - pathEscaped := html.EscapeString(path.Join(c.Path() + "/" + name)) - fi := fm[name] - auxStr := "dir" - className := "dir" - if !fi.IsDir() { - auxStr = fmt.Sprintf("file, %d bytes", fi.Size()) - className = "file" - } - c.Write(fmt.Sprintf(`
  • %s, %s, last modified %s
  • `, - pathEscaped, className, html.EscapeString(name), auxStr, fi.ModTime())) - } - c.Write("
") - - c.Type("html") - - return nil -} diff --git a/middleware/filesystem.md b/middleware/filesystem.md deleted file mode 100644 index 7befa9c7..00000000 --- a/middleware/filesystem.md +++ /dev/null @@ -1,150 +0,0 @@ -# FileSystem - -FileSystem middleware for Fiber - -### Example -The middleware packages comes with the official Fiber framework. -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) -``` - -### Signature -```go -embed.New(config ...embed.Config) func(c *fiber.Ctx) -``` - -### Config -| Property | Type | Description | Default | -| :--- | :--- | :--- | :--- | -| Index | `string` | Index file name | `index.html` | -| Browse | `bool` | Enable directory browsing | `false` | -| Root | `http.FileSystem` | http.FileSystem to use | `nil` | -| ErrorHandler | `func(*fiber.Ctx, error)` | Error handler | `InternalServerError` | - -### pkger - -```go -package main - -import ( - "net/http" - - "github.com/gofiber/fiber" - "github.com/gofiber/middleware/filesystem" -) - -func main() { - app := fiber.New() - - // Pass a FileSystem - app.Use("/assets", middleware.FileSystem(http.Dir("./assets"))) - - // Define the index file for serving a directory - app.Use("/assets", middleware.FileSystem(http.Dir("./assets"), "index.html")) - - // Enable directory browsing - app.Use("/assets", middleware.FileSystem(http.Dir("./assets"), true)) - - // Pass a config - app.Use("/assets", middleware.FileSystem(middleware.FileSystemConfig{ - Root: http.Dir("./assets"), - Index: "index.html", - Browse: true, - })) - - app.Listen(8080) -} -``` - -### packr - -```go -package main - -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/middleware/filesystem" - - "github.com/gobuffalo/packr/v2" -) - -func main() { - app := fiber.New() - - app.Use("/assets", middleware.FileSystem(packr.New("Assets Box", "/assets"))) - - app.Listen(8080) -} -``` - -### go.rice - -```go -package main - -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/middleware/filesystem" - - "github.com/GeertJohan/go.rice" -) - -func main() { - app := fiber.New() - - app.Use("/assets", middleware.FileSystem(rice.MustFindBox("assets").HTTPBox())) - - app.Listen(8080) -} -``` - -### fileb0x - -```go -package main - -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/middleware/filesystem" - "/myEmbeddedFiles" -) - -func main() { - app := fiber.New() - - app.Use("/assets", middleware.FileSystem(myEmbeddedFiles.HTTP)) - - app.Listen(8080) -} -``` - -### statik - -```go -package main - -import ( - "log" - "github.com/gofiber/fiber" - "github.com/gofiber/middleware/filesystem" - - "/statik" - fs "github.com/rakyll/statik/fs" -) - -func main() { - statik, err := fs.New() - if err != nil { - log.Fatal(err) - } - - app := fiber.New() - - app.Use("/", middleware.FileSystem.New(statikFS)) - - app.Listen(8080) -} -``` \ No newline at end of file diff --git a/middleware/filesystem/README.md b/middleware/filesystem/README.md new file mode 100644 index 00000000..60c4e887 --- /dev/null +++ b/middleware/filesystem/README.md @@ -0,0 +1,184 @@ +# Filesystem middleware +Filesystem middleware for [Fiber](https://github.com/gofiber/fiber) that enables you to serve files from a directory. + +### Table of Contents +- [Signatures](#signatures) +- [Examples](#examples) +- [Config](#config) +- [Default Config](#default-config) + + +### Signatures +```go +func New(config Config) fiber.Handler +``` + +### Examples +Import the middleware package that is part of the Fiber web framework +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/filesystem" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: +```go +// Provide a minimal config +app.Use(filesystem.New(filesystem.Config{ + Root: http.Dir("./assets") +})) + +// Or extend your config for customization +app.Use(filesystem.New(filesystem.Config{ + Root: http.Dir("./assets"), + Index: "index.html", + Browse: true, + NotFoundFile: "404.html" +})) +``` + + +## packr +https://github.com/gobuffalo/packr + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/filesystem" + + "github.com/gobuffalo/packr/v2" +) + +func main() { + app := fiber.New() + + app.Use("/assets", filesystem.New(filesystem.Config{ + Root: packr.New("Assets Box", "/assets"), + }) + + app.Listen(":3000") +} +``` + +## go.rice +https://github.com/GeertJohan/go.rice + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/filesystem" + + "github.com/GeertJohan/go.rice" +) + +func main() { + app := fiber.New() + + app.Use("/assets", filesystem.New(filesystem.Config{ + Root: rice.MustFindBox("assets").HTTPBox(), + }) + + app.Listen(":3000") +} +``` + +## fileb0x +https://github.com/UnnoTed/fileb0x + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/filesystem" + + "/myEmbeddedFiles" +) + +func main() { + app := fiber.New() + + app.Use("/assets", filesystem.New(filesystem.Config{ + Root: myEmbeddedFiles.HTTP, + }) + + app.Listen(":3000") +} +``` + +## statik +https://github.com/rakyll/statik + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/filesystem" + + "/statik" + fs "github.com/rakyll/statik/fs" +) + +func main() { + statik, err := fs.New() + if err != nil { + panic(err) + } + + app := fiber.New() + + app.Use("/", filesystem.New(filesystem.Config{ + Root: statikFS, + }) + + app.Listen(":3000") +} +``` + +### Config +```go +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Root is a FileSystem that provides access + // to a collection of files and directories. + // + // Required. Default: nil + Root http.FileSystem + + // Index file for serving a directory. + // + // Optional. Default: "index.html" + Index string + + // Enable directory browsing. + // + // Optional. Default: false + Browse bool + + // File to return if path is not found. Useful for SPA's. + // + // Optional. Default: "" + NotFoundFile string +} +``` + +### Default Config +```go +var ConfigDefault = Config{ + Next: nil, + Root: nil, + Index: "/index.html", + Browse: false, +} +``` \ No newline at end of file diff --git a/middleware/filesystem/filesystem.go b/middleware/filesystem/filesystem.go new file mode 100644 index 00000000..f4f3f435 --- /dev/null +++ b/middleware/filesystem/filesystem.go @@ -0,0 +1,166 @@ +package filesystem + +import ( + "log" + "net/http" + "os" + "strings" + "sync" + + "github.com/gofiber/fiber/v2" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Root is a FileSystem that provides access + // to a collection of files and directories. + // + // Required. Default: nil + Root http.FileSystem + + // Index file for serving a directory. + // + // Optional. Default: "index.html" + Index string + + // Enable directory browsing. + // + // Optional. Default: false + Browse bool + + // File to return if path is not found. Useful for SPA's. + // + // Optional. Default: "" + NotFoundFile string +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Next: nil, + Root: nil, + Index: "/index.html", + Browse: false, +} + +// New creates a new middleware handler +func New(config Config) fiber.Handler { + // Set config + cfg := config + + // Set default values + if cfg.Next == nil { + cfg.Next = ConfigDefault.Next + } + if cfg.Index == "" { + cfg.Index = ConfigDefault.Index + } + if !strings.HasPrefix(cfg.Index, "/") { + cfg.Index = "/" + cfg.Index + } + if cfg.Root == nil { + log.Fatal("filesystem: Root cannot be nil") + } + + var once sync.Once + var prefix string + + // Return new handler + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + method := c.Method() + + // We only serve static assets on GET or HEAD methods + if method != fiber.MethodGet && method != fiber.MethodHead { + return c.Next() + } + + // Set prefix once + once.Do(func() { + prefix = c.Route().Path + }) + + // Strip prefix + path := strings.TrimPrefix(c.Path(), prefix) + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + + file, err := cfg.Root.Open(path) + if err != nil && os.IsNotExist(err) && cfg.NotFoundFile != "" { + file, err = cfg.Root.Open(cfg.NotFoundFile) + } + + if err != nil { + if os.IsNotExist(err) { + return c.Status(fiber.StatusNotFound).Next() + } + return err + } + + stat, err := file.Stat() + if err != nil { + return err + } + + // Serve index if path is directory + if stat.IsDir() { + indexPath := strings.TrimSuffix(path, "/") + cfg.Index + index, err := cfg.Root.Open(indexPath) + if err == nil { + indexStat, err := index.Stat() + if err == nil { + file = index + stat = indexStat + } + } + } + + // Browse directory if no index found and browsing is enabled + if stat.IsDir() { + if cfg.Browse { + if err := dirList(c, file); err != nil { + return err + } + return nil + } + return fiber.ErrForbidden + } + + modTime := stat.ModTime() + contentLength := int(stat.Size()) + + // Set Content Type header + c.Type(getFileExtension(stat.Name())) + + // Set Last Modified header + if !modTime.IsZero() { + c.Set(fiber.HeaderLastModified, modTime.UTC().Format(http.TimeFormat)) + } + + if method == fiber.MethodGet { + c.Response().SetBodyStream(file, contentLength) + return nil + } + if method == fiber.MethodHead { + c.Request().ResetBody() + // Fasthttp should skipbody by default if HEAD? + c.Response().SkipBody = true + c.Response().Header.SetContentLength(contentLength) + if err := file.Close(); err != nil { + return err + } + return nil + } + + return c.Next() + } +} diff --git a/middleware/filesystem/filesystem_test.go b/middleware/filesystem/filesystem_test.go new file mode 100644 index 00000000..7c0661a4 --- /dev/null +++ b/middleware/filesystem/filesystem_test.go @@ -0,0 +1,112 @@ +package filesystem + +import ( + "net/http" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +// go test -run Test_FileSystem +func Test_FileSystem(t *testing.T) { + app := fiber.New() + + app.Use("/test", New(Config{ + Root: http.Dir("../../.github/testdata/fs"), + })) + + app.Use("/dir", New(Config{ + Root: http.Dir("../../.github/testdata/fs"), + Browse: true, + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + + app.Use("/spatest", New(Config{ + Root: http.Dir("../../.github/testdata/fs"), + NotFoundFile: "index.html", + })) + + tests := []struct { + name string + url string + statusCode int + contentType string + modifiedTime string + }{ + { + name: "Should be returns status 200 with suitable content-type", + url: "/test/index.html", + statusCode: 200, + contentType: "text/html", + }, + { + name: "Should be returns status 200 with suitable content-type", + url: "/test", + statusCode: 200, + contentType: "text/html", + }, + { + name: "Should be returns status 200 with suitable content-type", + url: "/test/css/style.css", + statusCode: 200, + contentType: "text/css", + }, + { + name: "Should be returns status 404", + url: "/test/nofile.js", + statusCode: 404, + }, + { + name: "Should be returns status 404", + url: "/test/nofile", + statusCode: 404, + }, + { + name: "Should be returns status 200", + url: "/", + statusCode: 200, + contentType: "text/plain; charset=utf-8", + }, + { + name: "Should be returns status 403", + url: "/test/img", + statusCode: 403, + }, + { + name: "Should list the directory contents", + url: "/dir/img", + statusCode: 200, + contentType: "text/html", + }, + { + name: "Should be returns status 200", + url: "/dir/img/fiber.png", + statusCode: 200, + contentType: "image/png", + }, + { + name: "Should be return status 200", + url: "/spatest/doesnotexist", + statusCode: 200, + contentType: "text/html", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, _ := http.NewRequest("GET", tt.url, nil) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, tt.statusCode, resp.StatusCode) + + if tt.contentType != "" { + ct := resp.Header.Get("Content-Type") + utils.AssertEqual(t, tt.contentType, ct) + } + }) + } +} diff --git a/middleware/filesystem/utils.go b/middleware/filesystem/utils.go new file mode 100644 index 00000000..3d20554b --- /dev/null +++ b/middleware/filesystem/utils.go @@ -0,0 +1,65 @@ +package filesystem + +import ( + "fmt" + "html" + "net/http" + "os" + "path" + "sort" + "strings" + + "github.com/gofiber/fiber/v2" +) + +func getFileExtension(path string) string { + n := strings.LastIndexByte(path, '.') + if n < 0 { + return "" + } + return path[n:] +} + +func dirList(c *fiber.Ctx, f http.File) error { + fileinfos, err := f.Readdir(-1) + if err != nil { + return err + } + + fm := make(map[string]os.FileInfo, len(fileinfos)) + filenames := make([]string, 0, len(fileinfos)) + for _, fi := range fileinfos { + name := fi.Name() + fm[name] = fi + filenames = append(filenames, name) + } + + basePathEscaped := html.EscapeString(c.Path()) + fmt.Fprintf(c, "%s", basePathEscaped) + fmt.Fprintf(c, "

%s

", basePathEscaped) + fmt.Fprint(c, "
    ") + + if len(basePathEscaped) > 1 { + parentPathEscaped := html.EscapeString(c.Path() + "/..") + fmt.Fprintf(c, `
  • ..
  • `, parentPathEscaped) + } + + sort.Strings(filenames) + for _, name := range filenames { + pathEscaped := html.EscapeString(path.Join(c.Path() + "/" + name)) + fi := fm[name] + auxStr := "dir" + className := "dir" + if !fi.IsDir() { + auxStr = fmt.Sprintf("file, %d bytes", fi.Size()) + className = "file" + } + fmt.Fprintf(c, `
  • %s, %s, last modified %s
  • `, + pathEscaped, className, html.EscapeString(name), auxStr, fi.ModTime()) + } + fmt.Fprint(c, "
") + + c.Type("html") + + return nil +} diff --git a/middleware/filesystem_test.go b/middleware/filesystem_test.go deleted file mode 100644 index c870d7c1..00000000 --- a/middleware/filesystem_test.go +++ /dev/null @@ -1 +0,0 @@ -package middleware diff --git a/middleware/limiter/README.md b/middleware/limiter/README.md new file mode 100644 index 00000000..f45a38f7 --- /dev/null +++ b/middleware/limiter/README.md @@ -0,0 +1,96 @@ +# Limiter +Limiter middleware for [Fiber](https://github.com/gofiber/fiber) used to limit repeated requests to public APIs and/or endpoints such as password reset etc. Also useful for API clients, web crawling, or other tasks that need to be throttled. + +**Note: this module does not share state with other processes/servers by default.** + +### Table of Contents +- [Signatures](#signatures) +- [Examples](#examples) +- [Config](#config) +- [Default Config](#default-config) + + +### Signatures +```go +func New(config ...Config) fiber.Handler +``` + +### Examples +Import the middleware package that is part of the Fiber web framework +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/limiter" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: +```go +// Default middleware config +app.Use(limiter.New()) + +// Or extend your config for customization +app.Use(limiter.New(limiter.Config{ + Next: func(c *fiber.Ctx) bool { + return c.IP() == "127.0.0.1" + }, + Max: 20, + Duration: 30 * time.Second, + Key: func(c *fiber.Ctx) string { + return c.Get("x-forwarded-for") + }, + LimitReached: func(c *fiber.Ctx) error { + return c.SendFile("./toofast.html") + }, +})) +``` + +### Config +```go +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Max number of recent connections during `Duration` seconds before sending a 429 response + // + // Default: 5 + Max int + + // Duration is the time on how long to keep records of requests in memory + // + // Default: time.Minute + Duration time.Duration + + // Key allows you to generate custom keys, by default c.IP() is used + // + // Default: func(c *fiber.Ctx) string { + // return c.IP() + // } + Key func(*fiber.Ctx) string + + // LimitReached is called when a request hits the limit + // + // Default: func(c *fiber.Ctx) error { + // return c.SendStatus(fiber.StatusTooManyRequests) + // } + LimitReached fiber.Handler +} +``` + +### Default Config +```go +var ConfigDefault = Config{ + Next: nil, + Max: 5, + Duration: time.Minute, + Key: func(c *fiber.Ctx) string { + return c.IP() + }, + LimitReached: func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusTooManyRequests) + }, +} +``` \ No newline at end of file diff --git a/middleware/limiter/limiter.go b/middleware/limiter/limiter.go new file mode 100644 index 00000000..4e86db48 --- /dev/null +++ b/middleware/limiter/limiter.go @@ -0,0 +1,154 @@ +package limiter + +import ( + "strconv" + "sync" + "time" + + "github.com/gofiber/fiber/v2" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Max number of recent connections during `Duration` seconds before sending a 429 response + // + // Default: 5 + Max int + + // Duration is the time on how long to keep records of requests in memory + // + // Default: time.Minute + Duration time.Duration + + // Key allows you to generate custom keys, by default c.IP() is used + // + // Default: func(c *fiber.Ctx) string { + // return c.IP() + // } + Key func(*fiber.Ctx) string + + // LimitReached is called when a request hits the limit + // + // Default: func(c *fiber.Ctx) error { + // return c.SendStatus(fiber.StatusTooManyRequests) + // } + LimitReached fiber.Handler +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Next: nil, + Max: 5, + Duration: time.Minute, + Key: func(c *fiber.Ctx) string { + return c.IP() + }, + LimitReached: func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusTooManyRequests) + }, +} + +// X-RateLimit-* headers +const ( + xRateLimitLimit = "X-RateLimit-Limit" + xRateLimitRemaining = "X-RateLimit-Remaining" + xRateLimitReset = "X-RateLimit-Reset" +) + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + // Set default config + cfg := ConfigDefault + + // Override config if provided + if len(config) > 0 { + cfg = config[0] + + // Set default values + if cfg.Next == nil { + cfg.Next = ConfigDefault.Next + } + if cfg.Max <= 0 { + cfg.Max = ConfigDefault.Max + } + if int(cfg.Duration.Seconds()) <= 0 { + cfg.Duration = ConfigDefault.Duration + } + if cfg.Key == nil { + cfg.Key = ConfigDefault.Key + } + if cfg.LimitReached == nil { + cfg.LimitReached = ConfigDefault.LimitReached + } + } + + // Limiter settings + var max = strconv.Itoa(cfg.Max) + var hits = make(map[string]int) + var reset = make(map[string]int) + var timestamp = int(time.Now().Unix()) + var duration = int(cfg.Duration.Seconds()) + + // mutex for parallel read and write access + mux := &sync.Mutex{} + + // Return new handler + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Get key (default is the remote IP) + key := cfg.Key(c) + + // Lock map + mux.Lock() + + // Set unix timestamp if not exist + if reset[key] == 0 { + reset[key] = timestamp + duration + } else if timestamp >= reset[key] { + hits[key] = 0 + reset[key] = timestamp + duration + } + + // Increment key hits + hits[key]++ + + // Get current hits + hitCount := hits[key] + + // Calculate when it resets in seconds + resetTime := reset[key] - timestamp + + // Unlock map + mux.Unlock() + + // Set how many hits we have left + remaining := cfg.Max - hitCount + + // Check if hits exceed the cfg.Max + if remaining < 0 { + // Return response with Retry-After header + // https://tools.ietf.org/html/rfc6584 + c.Set(fiber.HeaderRetryAfter, strconv.Itoa(resetTime)) + + // Call LimitReached handler + return cfg.LimitReached(c) + } + + // We can continue, update RateLimit headers + c.Set(xRateLimitLimit, max) + c.Set(xRateLimitRemaining, strconv.Itoa(remaining)) + c.Set(xRateLimitReset, strconv.Itoa(resetTime)) + + // Continue stack + return c.Next() + } +} diff --git a/middleware/limiter/limiter_test.go b/middleware/limiter/limiter_test.go new file mode 100644 index 00000000..b330c5a5 --- /dev/null +++ b/middleware/limiter/limiter_test.go @@ -0,0 +1,83 @@ +package limiter + +import ( + "io/ioutil" + "math/rand" + "net/http" + "net/http/httptest" + "sync" + "testing" + "time" + + "github.com/gofiber/fiber/v2/utils" + + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +// go test -run Test_Limiter_Concurrency -race -v +func Test_Limiter_Concurrency(t *testing.T) { + app := fiber.New() + + app.Use(New(Config{ + Max: 100, + Duration: 1 * time.Minute, + })) + + app.Get("/", func(c *fiber.Ctx) error { + // random delay between the requests + time.Sleep(time.Duration(rand.Intn(10000)) * time.Microsecond) + return c.SendString("Hello tester!") + }) + + var wg sync.WaitGroup + singleRequest := func(wg *sync.WaitGroup) { + defer wg.Done() + resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/", nil)) + if err != nil { + t.Fatal(err) + } + if resp.StatusCode != http.StatusOK { + t.Fatalf("Unexpected status code %v", resp.StatusCode) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil || "Hello tester!" != string(body) { + t.Fatalf("Unexpected body %v", string(body)) + } + } + + for i := 0; i <= 50; i++ { + wg.Add(1) + go singleRequest(&wg) + } + + wg.Wait() +} + +// go test -v -run=^$ -bench=Benchmark_Limiter -benchmem -count=4 +func Benchmark_Limiter(b *testing.B) { + app := fiber.New() + + app.Use(New(Config{ + Max: 100, + Duration: 60 * time.Second, + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + + h := app.Handler() + + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod("GET") + fctx.Request.SetRequestURI("/") + + b.ResetTimer() + + for n := 0; n < b.N; n++ { + h(fctx) + } + + utils.AssertEqual(b, "100", string(fctx.Response.Header.Peek("X-RateLimit-Limit"))) +} diff --git a/middleware/logger.go b/middleware/logger.go deleted file mode 100644 index c210a32a..00000000 --- a/middleware/logger.go +++ /dev/null @@ -1,464 +0,0 @@ -package middleware - -import ( - "bytes" - "fmt" - "io" - "os" - "strconv" - "strings" - "sync/atomic" - "time" - - fiber "github.com/gofiber/fiber" - utils "github.com/gofiber/utils" - colorable "github.com/mattn/go-colorable" - isatty "github.com/mattn/go-isatty" - bytebufferpool "github.com/valyala/bytebufferpool" -) - -// Middleware types -type ( - // LoggerConfig defines the config for Logger middleware. - LoggerConfig struct { - // Next defines a function to skip this middleware if returned true. - Next func(*fiber.Ctx) bool - - // Format defines the logging tags - // - // - pid - // - time - // - ip - // - ips - // - url - // - host - // - method - // - methodColored - // - path - // - protocol - // - route - // - referer - // - ua - // - latency - // - status - // - statusColored - // - body - // - error - // - bytesSent - // - bytesReceived - // - header: - // - query: - // - form: - // - cookie: - // - // Optional. Default: #${pid} - ${time} ${status} - ${latency} ${method} ${path}\n - Format string - - // TimeFormat https://programming.guide/go/format-parse-string-time-date-example.html - // - // Optional. Default: 2006/01/02 15:04:05 - TimeFormat string - - // TimeZone can be specified, such as "UTC" and "America/New_York" and "Asia/Chongqing", etc - // - // Optional. Default: Local - TimeZone string - - // Output is a writter where logs are written - // - // Default: os.Stderr - Output io.Writer - - // Colors are only supported if no custom Output is given - enableColors bool - - // timeZoneLocation holds the compiled timezone - timeZoneLocation *time.Location - } -) - -// Logger variables -const ( - LoggerTagPid = "pid" - LoggerTagTime = "time" - LoggerTagReferer = "referer" - LoggerTagProtocol = "protocol" - LoggerTagIP = "ip" - LoggerTagIPs = "ips" - LoggerTagHost = "host" - LoggerTagMethod = "method" - LoggerTagPath = "path" - LoggerTagURL = "url" - LoggerTagUA = "ua" - LoggerTagLatency = "latency" - LoggerTagStatus = "status" - LoggerTagBody = "body" - LoggerTagBytesSent = "bytesSent" - LoggerTagBytesReceived = "bytesReceived" - LoggerTagRoute = "route" - LoggerTagError = "error" - LoggerTagHeader = "header:" - LoggerTagQuery = "query:" - LoggerTagForm = "form:" - LoggerTagCookie = "cookie:" - LoggerTagColorBlack = "black" - LoggerTagColorRed = "red" - LoggerTagColorGreen = "green" - LoggerTagColorYellow = "yellow" - LoggerTagColorBlue = "blue" - LoggerTagColorMagenta = "magenta" - LoggerTagColorCyan = "cyan" - LoggerTagColorWhite = "white" - LoggerTagColorReset = "resetColor" - // LoggerTagStatusColor = "statusColor" - // LoggerTagMethodColor = "methodColor" -) - -// NEW : Color variables -const ( - cBlack = "\u001b[90m" - cRed = "\u001b[91m" - cGreen = "\u001b[92m" - cYellow = "\u001b[93m" - cBlue = "\u001b[94m" - cMagenta = "\u001b[95m" - cCyan = "\u001b[96m" - cWhite = "\u001b[97m" - cReset = "\u001b[0m" -) - -// for colorizing response status and request method -var ( - statusColor string - responseStatus int - methodColor string - requestMethod string -) - -// LoggerConfigDefault is the default config -var LoggerConfigDefault = LoggerConfig{ - Next: nil, - Format: "#${pid} - ${time} ${status} - ${latency} ${method} ${path}\n", - TimeFormat: "2006/01/02 15:04:05", - TimeZone: "Local", - Output: os.Stderr, -} - -/* -Logger allows the following config arguments in any order: - - Logger() - - Logger(next func(*fiber.Ctx) bool) - - Logger(output io.Writer) - - Logger(format string) - - Logger(timeZone string) - - Logger(timeFormat string) - - Logger(config LoggerConfig) -*/ -func Logger(options ...interface{}) fiber.Handler { - // Create default config - var config = LoggerConfig{} - // Assert options if provided to adjust the config - if len(options) > 0 { - for i := range options { - switch opt := options[i].(type) { - case func(*fiber.Ctx) bool: - config.Next = opt - case string: - if strings.Contains(opt, "${") { - config.Format = opt - } else if tzl := getTimeZoneLocation(opt); tzl != nil { - config.TimeZone = opt - config.timeZoneLocation = tzl - } else { - config.TimeFormat = opt - } - case io.Writer: - config.Output = opt - case LoggerConfig: - config = opt - default: - panic("Logger: the following option types are allowed: string, io.Writer, LoggerConfig") - } - } - } - // Return logger - return logger(config) -} - -func logger(config LoggerConfig) fiber.Handler { - // Set config default values - if config.Format == "" { - config.Format = LoggerConfigDefault.Format - } - if config.TimeZone == "" { - config.TimeZone = LoggerConfigDefault.TimeZone - } - if config.TimeFormat == "" { - config.TimeFormat = LoggerConfigDefault.TimeFormat - } - if config.Output == nil { - // Check if colors should be disabled - if os.Getenv("TERM") == "dumb" || - (!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) { - config.Output = LoggerConfigDefault.Output - } else { - config.enableColors = true - config.Output = colorable.NewColorableStderr() - - } - } - - var tmpl loggerTemplate - tmpl.new(config.Format, "${", "}") - - var timestamp atomic.Value - timestamp.Store(nowTimeString(config.timeZoneLocation, config.TimeFormat)) - // Update date/time every 750 milliseconds in a separate go routine - if strings.Contains(config.Format, "${time}") { - go func() { - for { - time.Sleep(750 * time.Millisecond) - timestamp.Store(nowTimeString(config.timeZoneLocation, config.TimeFormat)) - } - }() - } - pid := fmt.Sprintf("%-5s", strconv.Itoa(os.Getpid())) - // Return handler - return func(c *fiber.Ctx) { - // Don't execute the middleware if Next returns true - if config.Next != nil && config.Next(c) { - c.Next() - return - } - // Middleware logic... - start := time.Now() - // handle request - c.Next() - // build log - stop := time.Now() - // Get new buffer - buf := bytebufferpool.Get() - _, err := tmpl.executeFunc(buf, func(w io.Writer, tag string) (int, error) { - switch tag { - case LoggerTagTime: - return buf.WriteString(timestamp.Load().(string)) - case LoggerTagReferer: - return buf.WriteString(c.Get(fiber.HeaderReferer)) - case LoggerTagProtocol: - return buf.WriteString(c.Protocol()) - case LoggerTagPid: - return buf.WriteString(pid) - case LoggerTagIP: - return buf.WriteString(c.IP()) - case LoggerTagIPs: - return buf.WriteString(c.Get(fiber.HeaderXForwardedFor)) - case LoggerTagHost: - return buf.WriteString(c.Hostname()) - case LoggerTagPath: - return buf.WriteString(c.Path()) - case LoggerTagURL: - return buf.WriteString(c.OriginalURL()) - case LoggerTagUA: - return buf.WriteString(c.Get(fiber.HeaderUserAgent)) - case LoggerTagLatency: - return buf.WriteString(fmt.Sprintf("%-6s", stop.Sub(start).Round(1*time.Millisecond))) - // return buf.WriteString(stop.Sub(start).String()) - case LoggerTagBody: - return buf.WriteString(c.Body()) - case LoggerTagBytesReceived: - return buf.WriteString(strconv.Itoa(len(c.Fasthttp.Request.Body()))) - case LoggerTagBytesSent: - return buf.WriteString(strconv.Itoa(len(c.Fasthttp.Response.Body()))) - case LoggerTagRoute: - return buf.WriteString(c.Route().Path) - case LoggerTagError: - if c.Error() != nil { - return buf.WriteString(c.Error().Error()) - } - case LoggerTagColorBlack: - return buf.WriteString(cBlack) - case LoggerTagColorRed: - return buf.WriteString(cRed) - case LoggerTagColorGreen: - return buf.WriteString(cGreen) - case LoggerTagColorYellow: - return buf.WriteString(cYellow) - case LoggerTagColorBlue: - return buf.WriteString(cBlue) - case LoggerTagColorMagenta: - return buf.WriteString(cMagenta) - case LoggerTagColorCyan: - return buf.WriteString(cCyan) - case LoggerTagColorWhite: - return buf.WriteString(cWhite) - case LoggerTagColorReset: - return buf.WriteString(cReset) - case LoggerTagStatus: - responseStatus = c.Fasthttp.Response.StatusCode() - if !config.enableColors { - return buf.WriteString(strconv.Itoa(responseStatus)) - } - switch { - case responseStatus >= 200 && responseStatus < 300: - statusColor = cGreen - case responseStatus >= 300 && responseStatus < 400: - statusColor = cBlue - case responseStatus >= 400 && responseStatus < 500: - statusColor = cYellow - default: - statusColor = cRed - } - return buf.WriteString(statusColor + strconv.Itoa(responseStatus) + cReset) - case LoggerTagMethod: - requestMethod = c.Method() - if !config.enableColors { - return buf.WriteString(requestMethod) - } - switch requestMethod { - case fiber.MethodGet: - methodColor = cGreen - case fiber.MethodPost: - methodColor = cCyan - case fiber.MethodPut: - methodColor = cYellow - case fiber.MethodDelete: - methodColor = cRed - case fiber.MethodPatch: - methodColor = cBlue - case fiber.MethodHead: - methodColor = cMagenta - case fiber.MethodOptions: - methodColor = cBlack - default: - methodColor = cReset - } - return buf.WriteString(fmt.Sprintf("%s%7s%s", methodColor, requestMethod, cReset)) - //return buf.WriteString(methodColor + requestMethod + cReset) - default: - switch { - case strings.HasPrefix(tag, LoggerTagHeader): - return buf.WriteString(c.Get(tag[7:])) - case strings.HasPrefix(tag, LoggerTagQuery): - return buf.WriteString(c.Query(tag[6:])) - case strings.HasPrefix(tag, LoggerTagForm): - return buf.WriteString(c.FormValue(tag[5:])) - case strings.HasPrefix(tag, LoggerTagCookie): - return buf.WriteString(c.Cookies(tag[7:])) - } - } - return 0, nil - }) - if err != nil { - _, _ = buf.WriteString(err.Error()) - } - if _, err := config.Output.Write(buf.Bytes()); err != nil { - fmt.Println(err) - } - bytebufferpool.Put(buf) - } -} - -func nowTimeString(tzl *time.Location, layout string) string { - // This is different from Golang's time package which returns UTC, and Local is better than it - if tzl == nil { - return time.Now().Format(layout) - } - return time.Now().In(tzl).Format(layout) -} - -// Use Golang's time package to determine whether the TimeZone is available -func getTimeZoneLocation(name string) *time.Location { - tz, _ := time.LoadLocation(name) - return tz -} - -// MIT License fasttemplate -// Copyright (c) 2015 Aliaksandr Valialkin -// https://github.com/valyala/fasttemplate/blob/master/LICENSE - -type ( - loggerTemplate struct { - template string - startTag string - endTag string - texts [][]byte - tags []string - } - loggerTagFunc func(w io.Writer, tag string) (int, error) -) - -func (t *loggerTemplate) new(template, startTag, endTag string) { - t.template = template - t.startTag = startTag - t.endTag = endTag - t.texts = t.texts[:0] - t.tags = t.tags[:0] - - if len(startTag) == 0 { - panic("startTag cannot be empty") - } - if len(endTag) == 0 { - panic("endTag cannot be empty") - } - - s := utils.GetBytes(template) - a := utils.GetBytes(startTag) - b := utils.GetBytes(endTag) - - tagsCount := bytes.Count(s, a) - if tagsCount == 0 { - return - } - - if tagsCount+1 > cap(t.texts) { - t.texts = make([][]byte, 0, tagsCount+1) - } - if tagsCount > cap(t.tags) { - t.tags = make([]string, 0, tagsCount) - } - - for { - n := bytes.Index(s, a) - if n < 0 { - t.texts = append(t.texts, s) - break - } - t.texts = append(t.texts, s[:n]) - - s = s[n+len(a):] - n = bytes.Index(s, b) - if n < 0 { - panic(fmt.Errorf("cannot find end tag=%q in the template=%q starting from %q", endTag, template, s)) - } - - t.tags = append(t.tags, utils.GetString(s[:n])) - s = s[n+len(b):] - } -} - -func (t *loggerTemplate) executeFunc(w io.Writer, f loggerTagFunc) (int64, error) { - var nn int64 - - n := len(t.texts) - 1 - if n == -1 { - ni, err := w.Write(utils.GetBytes(t.template)) - return int64(ni), err - } - - for i := 0; i < n; i++ { - ni, err := w.Write(t.texts[i]) - nn += int64(ni) - if err != nil { - return nn, err - } - - ni, err = f(w, t.tags[i]) - nn += int64(ni) - if err != nil { - return nn, err - } - } - ni, err := w.Write(t.texts[n]) - nn += int64(ni) - return nn, err -} diff --git a/middleware/logger.md b/middleware/logger.md deleted file mode 100644 index fd24a904..00000000 --- a/middleware/logger.md +++ /dev/null @@ -1,104 +0,0 @@ -# Logger -HTTP request/response logger for Fiber - -### Example -The middleware packages comes with the official Fiber framework. -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) - -func main() { - // ... - - // Default Logger - app.Use(middleware.Logger()) - - // Pass a custom output - app.Use(middleware.Logger(os.Stdout)) - - // Pass a custom time zone - app.Use(middleware.Logger("UTC")) - - // Pass a custom timeformat - app.Use(middleware.Logger("15:04:05")) - - // Pass a custom format - app.Use(middleware.Logger("${time} ${method} ${path}")) - - // Pass a custom output + timeformat + format - app.Use(middleware.Logger(os.Stdout, "15:04:05", "${time} ${method} ${path}")) - - // Order does not matter - app.Use(middleware.Logger("${time} ${method} ${path}", os.Stdout, "15:04:05")) - - // Pass a custom config - app.Use(middleware.Logger(middleware.LoggerConfig{ - Format: "${time} ${method} ${path}", - TimeFormat: "15:04:05", - TimeZone: "Asia/Chongqing", - Output: os.Stdout, - })) - - // ... -} -``` - -### Signature -```go -func Logger(options ...interface{}) fiber.Handler {} -``` - -### Config -```go -type LoggerConfig struct { - // Next defines a function to skip this middleware. - Next func(ctx *fiber.Ctx) bool - - // Format defines the logging tags - // - // - time - // - ip - // - ips - // - url - // - host - // - method - // - methodColor - // - path - // - protocol - // - route - // - referer - // - ua - // - latency - // - status - // - statusColor - // - body - // - error - // - bytesSent - // - bytesReceived - // - header: - // - query: - // - form: - // - cookie: - // - - e.g. black, red, blue, yellow, cyan, magenta, white, resetColor - // - // Optional. Default: ${time} ${method} ${path} - ${ip} - ${status} - ${latency}\n - Format string - - // TimeFormat https://programming.guide/go/format-parse-string-time-date-example.html - // - // Optional. Default: 15:04:05 - TimeFormat string - - // TimeZone can be specified, such as "UTC" and "America/New_York" and "Asia/Chongqing", etc - // - // Optional. Default: Local - TimeZone string - - // Output is a writter where logs are written - // - // Default: os.Stderr - Output io.Writer -} -``` \ No newline at end of file diff --git a/middleware/logger/README.md b/middleware/logger/README.md new file mode 100644 index 00000000..da35826d --- /dev/null +++ b/middleware/logger/README.md @@ -0,0 +1,116 @@ +# Logger +Logger middleware for [Fiber](https://github.com/gofiber/fiber) that logs HTTP request/response details. + +### Table of Contents +- [Signatures](#signatures) +- [Examples](#examples) +- [Config](#config) +- [Default Config](#default-config) +- [Constants](#constants) + +### Signatures +```go +func New(config ...Config) fiber.Handler +``` + +### Examples +Import the middleware package that is part of the Fiber web framework +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: +```go +// Default middleware config +app.Use(logger.New()) + +// Or extend your config for customization +app.Use(logger.New(logger.Config{ + Format: "${pid} ${status} - ${method} ${path}\n", + TimeFormat: "02-Jan-2006", + TimeZone: "America/New_York", + Output: os.Stdout, +})) +``` + +### Config +```go +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Format defines the logging tags + // + // Optional. Default: [${time}] ${status} - ${latency} ${method} ${path}\n + Format string + + // TimeFormat https://programming.guide/go/format-parse-string-time-date-example.html + // + // Optional. Default: 15:04:05 + TimeFormat string + + // TimeZone can be specified, such as "UTC" and "America/New_York" and "Asia/Chongqing", etc + // + // Optional. Default: "Local" + TimeZone string + // Output is a writter where logs are written + // + // Default: os.Stderr + Output io.Writer +} +``` + +### Default Config +```go +var ConfigDefault = Config{ + Next: nil, + Format: "[${time}] ${status} - ${latency} ${method} ${path}\n", + TimeFormat: "15:04:05", + TimeZone: "Local", + Output: os.Stderr, +} +``` + +### Constants +```go +// Logger variables +const ( + TagPid = "pid" + TagTime = "time" + TagReferer = "referer" + TagProtocol = "protocol" + TagIP = "ip" + TagIPs = "ips" + TagHost = "host" + TagMethod = "method" + TagPath = "path" + TagURL = "url" + TagUA = "ua" + TagLatency = "latency" + TagStatus = "status" + TagBody = "body" + TagBytesSent = "bytesSent" + TagBytesReceived = "bytesReceived" + TagRoute = "route" + TagError = "error" + TagHeader = "header:" + TagQuery = "query:" + TagForm = "form:" + TagCookie = "cookie:" + TagBlack = "black" + TagRed = "red" + TagGreen = "green" + TagYellow = "yellow" + TagBlue = "blue" + TagMagenta = "magenta" + TagCyan = "cyan" + TagWhite = "white" + TagReset = "reset" +) +``` \ No newline at end of file diff --git a/middleware/logger/logger.go b/middleware/logger/logger.go new file mode 100644 index 00000000..f9760988 --- /dev/null +++ b/middleware/logger/logger.go @@ -0,0 +1,277 @@ +package logger + +import ( + "io" + "os" + "strconv" + "strings" + "sync/atomic" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils/bytebufferpool" + "github.com/gofiber/fiber/v2/utils/fasttemplate" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Format defines the logging tags + // + // Optional. Default: [${time}] ${status} - ${latency} ${method} ${path}\n + Format string + + // TimeFormat https://programming.guide/go/format-parse-string-time-date-example.html + // + // Optional. Default: 15:04:05 + TimeFormat string + + // TimeZone can be specified, such as "UTC" and "America/New_York" and "Asia/Chongqing", etc + // + // Optional. Default: "Local" + TimeZone string + // Output is a writter where logs are written + // + // Default: os.Stderr + Output io.Writer + + haveLatency bool + timeZoneLocation *time.Location +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Next: nil, + Format: "[${time}] ${status} - ${latency} ${method} ${path}\n", + TimeFormat: "15:04:05", + TimeZone: "Local", + Output: os.Stderr, +} + +// Logger variables +const ( + TagPid = "pid" + TagTime = "time" + TagReferer = "referer" + TagProtocol = "protocol" + TagIP = "ip" + TagIPs = "ips" + TagHost = "host" + TagMethod = "method" + TagPath = "path" + TagURL = "url" + TagUA = "ua" + TagLatency = "latency" + TagStatus = "status" + TagBody = "body" + TagBytesSent = "bytesSent" + TagBytesReceived = "bytesReceived" + TagRoute = "route" + TagError = "error" + TagHeader = "header:" + TagQuery = "query:" + TagForm = "form:" + TagCookie = "cookie:" + TagBlack = "black" + TagRed = "red" + TagGreen = "green" + TagYellow = "yellow" + TagBlue = "blue" + TagMagenta = "magenta" + TagCyan = "cyan" + TagWhite = "white" + TagReset = "reset" +) + +// Color values +const ( + cBlack = "\u001b[90m" + cRed = "\u001b[91m" + cGreen = "\u001b[92m" + cYellow = "\u001b[93m" + cBlue = "\u001b[94m" + cMagenta = "\u001b[95m" + cCyan = "\u001b[96m" + cWhite = "\u001b[97m" + cReset = "\u001b[0m" +) + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + // Set default config + cfg := ConfigDefault + + // Override config if provided + if len(config) > 0 { + cfg = config[0] + + // Set default values + if cfg.Next == nil { + cfg.Next = ConfigDefault.Next + } + if cfg.Format == "" { + cfg.Format = ConfigDefault.Format + } + if cfg.TimeZone == "" { + cfg.TimeZone = ConfigDefault.TimeZone + } + if cfg.TimeFormat == "" { + cfg.TimeFormat = ConfigDefault.TimeFormat + } + if cfg.Output == nil { + cfg.Output = ConfigDefault.Output + } + } + + // Get timezone location + tz, err := time.LoadLocation(cfg.TimeZone) + if err != nil || tz == nil { + cfg.timeZoneLocation = time.Local + } else { + cfg.timeZoneLocation = tz + } + + // Check if format contains latency + cfg.haveLatency = strings.Contains(cfg.Format, "${latency}") + + // Create template parser + tmpl := fasttemplate.New(cfg.Format, "${", "}") + + // Create correct timeformat + var timestamp atomic.Value + timestamp.Store(time.Now().In(cfg.timeZoneLocation).Format(cfg.TimeFormat)) + + // Update date/time every 750 milliseconds in a separate go routine + if strings.Contains(cfg.Format, "${time}") { + go func() { + for { + time.Sleep(750 * time.Millisecond) + timestamp.Store(time.Now().In(cfg.timeZoneLocation).Format(cfg.TimeFormat)) + } + }() + } + + // Set PID once + pid := strconv.Itoa(os.Getpid()) + + // Set start and stop + var start, stop time.Time + + // Return new handler + return func(c *fiber.Ctx) (err error) { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + // Set latency start time + if cfg.haveLatency { + start = time.Now() + } + // Handle request, store err for logging + err = c.Next() + + // Set latency stop time + if cfg.haveLatency { + stop = time.Now() + } + + // Get new buffer + buf := bytebufferpool.Get() + + // Loop over template tags to replace it with the correct value + _, err = tmpl.ExecuteFunc(buf, func(w io.Writer, tag string) (int, error) { + switch tag { + case TagTime: + return buf.WriteString(timestamp.Load().(string)) + case TagReferer: + return buf.WriteString(c.Get(fiber.HeaderReferer)) + case TagProtocol: + return buf.WriteString(c.Protocol()) + case TagPid: + return buf.WriteString(pid) + case TagIP: + return buf.WriteString(c.IP()) + case TagIPs: + return buf.WriteString(c.Get(fiber.HeaderXForwardedFor)) + case TagHost: + return buf.WriteString(c.Hostname()) + case TagPath: + return buf.WriteString(c.Path()) + case TagURL: + return buf.WriteString(c.OriginalURL()) + case TagUA: + return buf.WriteString(c.Get(fiber.HeaderUserAgent)) + case TagLatency: + return buf.WriteString(stop.Sub(start).String()) + case TagBody: + return buf.Write(c.Body()) + case TagBytesReceived: + return buf.WriteString(strconv.Itoa(len(c.Request().Body()))) + case TagBytesSent: + return buf.WriteString(strconv.Itoa(len(c.Request().Body()))) + case TagRoute: + return buf.WriteString(c.Route().Path) + case TagStatus: + return buf.WriteString(strconv.Itoa(c.Response().StatusCode())) + case TagMethod: + return buf.WriteString(c.Method()) + case TagBlack: + return buf.WriteString(cBlack) + case TagRed: + return buf.WriteString(cRed) + case TagGreen: + return buf.WriteString(cGreen) + case TagYellow: + return buf.WriteString(cYellow) + case TagBlue: + return buf.WriteString(cBlue) + case TagMagenta: + return buf.WriteString(cMagenta) + case TagCyan: + return buf.WriteString(cCyan) + case TagWhite: + return buf.WriteString(cWhite) + case TagReset: + return buf.WriteString(cReset) + case TagError: + if err != nil { + return buf.WriteString(err.Error()) + } + return buf.WriteString("-") + default: + // Check if we have a value tag i.e.: "header:x-key" + switch { + case strings.HasPrefix(tag, TagHeader): + return buf.WriteString(c.Get(tag[7:])) + case strings.HasPrefix(tag, TagQuery): + return buf.WriteString(c.Query(tag[6:])) + case strings.HasPrefix(tag, TagForm): + return buf.WriteString(c.FormValue(tag[5:])) + case strings.HasPrefix(tag, TagCookie): + return buf.WriteString(c.Cookies(tag[7:])) + } + } + return 0, nil + }) + // Also write errors to the buffer + if err != nil { + _, _ = buf.WriteString(err.Error()) + } + // Write buffer to output + if _, err := cfg.Output.Write(buf.Bytes()); err != nil { + // Write error to output + if _, err := cfg.Output.Write([]byte(err.Error())); err != nil { + // There is something wrong with the given io.Writer + // TODO: What should we do here? + } + } + // Put buffer back to pool + bytebufferpool.Put(buf) + + return nil + } +} diff --git a/middleware/logger/logger__test.go b/middleware/logger/logger__test.go new file mode 100644 index 00000000..74bfe37b --- /dev/null +++ b/middleware/logger/logger__test.go @@ -0,0 +1,33 @@ +package logger + +import ( + "errors" + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" + "github.com/gofiber/fiber/v2/utils/bytebufferpool" +) + +// go test -run Test_Logger +func Test_Logger(t *testing.T) { + app := fiber.New() + + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app.Use(New(Config{ + Format: "${error}", + Output: buf, + })) + + app.Get("/", func(c *fiber.Ctx) error { + return errors.New("some random error") + }) + + resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusInternalServerError, resp.StatusCode) + utils.AssertEqual(t, "some random error", buf.String()) +} diff --git a/middleware/logger_test.go b/middleware/logger_test.go deleted file mode 100644 index a1d14602..00000000 --- a/middleware/logger_test.go +++ /dev/null @@ -1,218 +0,0 @@ -package middleware - -import ( - "errors" - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/gofiber/fiber" - "github.com/gofiber/utils" - "github.com/valyala/bytebufferpool" - "github.com/valyala/fasthttp" -) - -// go test -run Test_Middleware_Logger -func Test_Middleware_Logger(t *testing.T) { - format := "${ip}-${ips}-${url}-${host}-${method}-${path}-${protocol}-${route}-${referer}-${ua}-${status}-${body}-${error}-${bytesSent}-${bytesReceived}-${header:header}-${query:query}-${form:form}-${cookie:cookie}-${black}-${red}-${green}-${yellow}-${blue}-${magenta}-${cyan}-${white}-${resetColor}" - expect := "0.0.0.0--/test?query=query-example.com-POST-/test-http-/test-ref-ua-500-form=form-error-5-9-header-query-form-cookie-\u001b[90m-\u001b[91m-\u001b[92m-\u001b[93m-\u001b[94m-\u001b[95m-\u001b[96m-\u001b[97m-\u001b[0m" - - buf := bytebufferpool.Get() - defer bytebufferpool.Put(buf) - - app := fiber.New() - - app.Use(Logger(LoggerConfig{ - Format: format, - Output: buf, - })) - - app.Post("/test", func(ctx *fiber.Ctx) { - ctx.Next(errors.New("error")) - }) - - req := httptest.NewRequest("POST", "/test?query=query", strings.NewReader("form=form")) - req.Header.Set("header", "header") - req.Header.Set("Cookie", "cookie=cookie") - req.Header.Set("User-Agent", "ua") - req.Header.Set("Referer", "ref") - req.Header.Set("Content-type", "application/x-www-form-urlencoded") - - resp, err := app.Test(req) - utils.AssertEqual(t, nil, err, "app.Test(req)") - utils.AssertEqual(t, fiber.StatusInternalServerError, resp.StatusCode, "Status code") - utils.AssertEqual(t, expect, buf.String()) - -} - -func Test_Middleware_Logger_WithDefaultFormat(t *testing.T) { - // fake output - buf := bytebufferpool.Get() - defer bytebufferpool.Put(buf) - - LoggerConfigDefault.Output = buf - config := LoggerConfigDefault - config.Output = nil - - app := fiber.New(&fiber.Settings{DisableStartupMessage: true}) - app.Use(Logger(config)) - - app.Get("/", func(ctx *fiber.Ctx) { - ctx.SendStatus(fiber.StatusOK) - }) - - _, err := app.Test(httptest.NewRequest(http.MethodGet, "/", nil)) - utils.AssertEqual(t, nil, err, "app.Test(req)") - res := buf.String() - utils.AssertEqual(t, 48, len(res), fmt.Sprintf("Has length: %v, expected: %v, raw: %s", len(res), 48, res)) -} - -// go test -run Test_Middleware_Logger_Skip -func Test_Middleware_Logger_Skip(t *testing.T) { - buf := bytebufferpool.Get() - defer bytebufferpool.Put(buf) - - LoggerConfigDefault.Output = buf - - app := fiber.New() - - app.Use(Logger(func(_ *fiber.Ctx) bool { - return true - })) - - app.Get("/", func(_ *fiber.Ctx) {}) - - resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/", nil)) - utils.AssertEqual(t, nil, err, "app.Test(req)") - utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") - utils.AssertEqual(t, 0, buf.Len(), "buf.Len()") -} - -// go test -run Test_Middleware_Logger_Options_And_WithConfig -func Test_Middleware_Logger_Options_And_WithConfig(t *testing.T) { - t.Parallel() - - buf := bytebufferpool.Get() - defer bytebufferpool.Put(buf) - - LoggerConfigDefault.Output = buf - - loggers := []fiber.Handler{ - Logger(buf), - Logger("15:04:05"), - Logger("${time} ${method} ${path} - ${ip} - ${status} - ${latency}\n"), - Logger(LoggerConfig{Output: buf}), - Logger("UTC"), - } - - for i, logger := range loggers { - buf.Reset() - - app := fiber.New() - - app.Use(logger) - - app.Get("/", func(_ *fiber.Ctx) {}) - - resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/", nil)) - utils.AssertEqual(t, nil, err, "app.Test(req)") - utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") - res := buf.String() - - if i == 0 { - utils.AssertEqual(t, 48, len(res), fmt.Sprintf("Has length: %v, expected: %v, raw: %s", len(res), 48, res)) - } else if i == 1 { - utils.AssertEqual(t, 37, len(res), fmt.Sprintf("Has length: %v, expected: %v, raw: %s", len(res), 37, res)) - } else if i == 2 { - utils.AssertEqual(t, 51, len(res), fmt.Sprintf("Has length: %v, expected: %v, raw: %s", len(res), 51, res)) - } else if i == 3 { - utils.AssertEqual(t, 48, len(res), fmt.Sprintf("Has length: %v, expected: %v, raw: %s", len(res), 48, res)) - } else if i == 4 { - utils.AssertEqual(t, 48, len(res), fmt.Sprintf("Has length: %v, expected: %v, raw: %s", len(res), 48, res)) - } - } -} - -// go test -run Test_Middleware_Logger_Panic -func Test_Middleware_Logger_Panic(t *testing.T) { - defer func() { - utils.AssertEqual(t, - "Logger: the following option types are allowed: string, io.Writer, LoggerConfig", - fmt.Sprintf("%s", recover())) - }() - - Logger(0) -} - -func Test_isTimeZone(t *testing.T) { - type args struct { - name string - } - tests := []struct { - name string - args args - want bool - }{ - { - "Empty", - args{""}, - true, - }, - { - "Local", - args{name: "Local"}, - true, - }, - { - "UTC", - args{name: "UTC"}, - true, - }, - { - "America/New_York", - args{name: "America/New_York"}, - true, - }, - { - "Asia/Chongqing", - args{"Asia/Chongqing"}, - true, - }, - { - "Time format", - args{name: "2006-01-02 15:04:05"}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - utils.AssertEqual(t, getTimeZoneLocation(tt.args.name) != nil, tt.want) - }) - } -} - -// go test -v -run=^$ -bench=Benchmark_Middleware_Logger -benchmem -count=4 -func Benchmark_Middleware_Logger(b *testing.B) { - - buf := bytebufferpool.Get() - defer bytebufferpool.Put(buf) - - app := fiber.New() - app.Use(Logger(LoggerConfig{ - Output: buf, - })) - - app.Get("/", func(c *fiber.Ctx) {}) - handler := app.Handler() - - c := &fasthttp.RequestCtx{} - c.Request.SetRequestURI("/") - - b.ReportAllocs() - b.ResetTimer() - for n := 0; n < b.N; n++ { - handler(c) - } -} diff --git a/middleware/pprof/README.md b/middleware/pprof/README.md new file mode 100644 index 00000000..42163ca6 --- /dev/null +++ b/middleware/pprof/README.md @@ -0,0 +1,25 @@ +# Pprof +Pprof middleware for [Fiber](https://github.com/gofiber/fiber) that serves via its HTTP server runtime profiling data in the format expected by the pprof visualization tool. The package is typically only imported for the side effect of registering its HTTP handlers. The handled paths all begin with /debug/pprof/. + +- [Signatures](#signatures) +- [Examples](#examples) + +### Signatures +```go +func New() fiber.Handler +``` + +### Example +Import the compress package that is part of the Fiber web framework +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/pprof" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: +```go +// Default middleware +app.Use(pprof.New()) +``` \ No newline at end of file diff --git a/middleware/pprof.go b/middleware/pprof/pprof.go similarity index 71% rename from middleware/pprof.go rename to middleware/pprof/pprof.go index 435d1b89..f122b73e 100644 --- a/middleware/pprof.go +++ b/middleware/pprof/pprof.go @@ -1,11 +1,11 @@ -package middleware +package pprof import ( "net/http/pprof" "strings" - fiber "github.com/gofiber/fiber" - fasthttpadaptor "github.com/valyala/fasthttp/fasthttpadaptor" + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp/fasthttpadaptor" ) // Set pprof adaptors @@ -23,44 +23,44 @@ var ( pprofThreadcreate = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Handler("threadcreate").ServeHTTP) ) -// Pprof will enabling profiling -func Pprof() fiber.Handler { - // Return handler - return func(c *fiber.Ctx) { +// New creates a new middleware handler +func New() fiber.Handler { + // Return new handler + return func(c *fiber.Ctx) error { path := c.Path() // We are only interested in /debug/pprof routes if len(path) < 12 || !strings.HasPrefix(path, "/debug/pprof") { - c.Next() - return + return c.Next() } // Switch to original path without stripped slashes switch path { case "/debug/pprof/": - c.Fasthttp.SetContentType(fiber.MIMETextHTML) - pprofIndex(c.Fasthttp) + c.Context().SetContentType(fiber.MIMETextHTML) + pprofIndex(c.Context()) case "/debug/pprof/cmdline": - pprofCmdline(c.Fasthttp) + pprofCmdline(c.Context()) case "/debug/pprof/profile": - pprofProfile(c.Fasthttp) + pprofProfile(c.Context()) case "/debug/pprof/symbol": - pprofSymbol(c.Fasthttp) + pprofSymbol(c.Context()) case "/debug/pprof/trace": - pprofTrace(c.Fasthttp) + pprofTrace(c.Context()) case "/debug/pprof/allocs": - pprofAllocs(c.Fasthttp) + pprofAllocs(c.Context()) case "/debug/pprof/block": - pprofBlock(c.Fasthttp) + pprofBlock(c.Context()) case "/debug/pprof/goroutine": - pprofGoroutine(c.Fasthttp) + pprofGoroutine(c.Context()) case "/debug/pprof/heap": - pprofHeap(c.Fasthttp) + pprofHeap(c.Context()) case "/debug/pprof/mutex": - pprofMutex(c.Fasthttp) + pprofMutex(c.Context()) case "/debug/pprof/threadcreate": - pprofThreadcreate(c.Fasthttp) + pprofThreadcreate(c.Context()) default: // pprof index only works with trailing slash - c.Redirect("/debug/pprof/", 302) + return c.Redirect("/debug/pprof/", 302) } + return nil } } diff --git a/middleware/proxy/README.md b/middleware/proxy/README.md new file mode 100644 index 00000000..8b3a2469 --- /dev/null +++ b/middleware/proxy/README.md @@ -0,0 +1,78 @@ +# Proxy +Proxy middleware for [Fiber](https://github.com/gofiber/fiber) that allows you to proxy requests to multiple hosts. + +### Table of Contents +- [Signatures](#signatures) +- [Examples](#examples) +- [Config](#config) +- [Default Config](#default-config) + + +### Signatures +```go +func New(config Config) fiber.Handler +``` + +### Examples +Import the middleware package that is part of the Fiber web framework +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/proxy" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: +```go +// Minimal config +app.Use(proxy.New(proxy.Config{ + Hosts: "gofiber.io:8080, gofiber.io:8081", +})) + +// Or extend your config for customization +app.Use(proxy.New(proxy.Config{ + Hosts: "gofiber.io:8080, gofiber.io:8081", + Before: func(c *fiber.Ctx) error { + c.Set("X-Real-IP", c.IP()) + return nil + }, +})) +``` + +### Config +```go +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Comma-separated list of upstream HTTP server host addresses, + // which are passed to Dial in a round-robin manner. + // + // Each address may contain port if default dialer is used. + // For example, + // + // - foobar.com:80 + // - foobar.com:443 + // - foobar.com:8080 + Hosts string + + // Before allows you to alter the request + Before fiber.Handler + + // After allows you to alter the response + After fiber.Handler +} +``` + +### Default Config +```go +var ConfigDefault = Config{ + Next: nil, + Hosts: "", + Before: nil, + After: nil, +} +``` diff --git a/middleware/proxy/proxy.go b/middleware/proxy/proxy.go new file mode 100644 index 00000000..337775ba --- /dev/null +++ b/middleware/proxy/proxy.go @@ -0,0 +1,99 @@ +package proxy + +import ( + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Comma-separated list of upstream HTTP server host addresses, + // which are passed to Dial in a round-robin manner. + // + // Each address may contain port if default dialer is used. + // For example, + // + // - foobar.com:80 + // - foobar.com:443 + // - foobar.com:8080 + Hosts string + + // Before allows you to alter the request + Before fiber.Handler + + // After allows you to alter the response + After fiber.Handler +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Next: nil, +} + +// New creates a new middleware handler +func New(config Config) fiber.Handler { + // Override config if provided + cfg := config + + // Set default values + if cfg.Next == nil { + cfg.Next = ConfigDefault.Next + } + if cfg.Hosts == "" { + return func(c *fiber.Ctx) error { + return c.Next() + } + } + + // Create host client + // https://godoc.org/github.com/valyala/fasthttp#HostClient + hostClient := fasthttp.HostClient{ + Addr: cfg.Hosts, + NoDefaultUserAgentHeader: true, + } + + // Return new handler + return func(c *fiber.Ctx) (err error) { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Set request and response + req := c.Request() + res := c.Response() + + // Don't proxy "Connection" header + req.Header.Del(fiber.HeaderConnection) + + // Modify request + if cfg.Before != nil { + if err = cfg.Before(c); err != nil { + return err + } + } + + // Forward request + if err = hostClient.Do(req, res); err != nil { + return err + } + + // Don't proxy "Connection" header + res.Header.Del(fiber.HeaderConnection) + + // Modify response + if cfg.After != nil { + if err = cfg.After(c); err != nil { + return err + } + } + + // Return nil to end proxying if no error + return nil + } +} diff --git a/middleware/proxy/proxy_test.go b/middleware/proxy/proxy_test.go new file mode 100644 index 00000000..c45d72c1 --- /dev/null +++ b/middleware/proxy/proxy_test.go @@ -0,0 +1,167 @@ +package proxy + +import ( + "fmt" + "io/ioutil" + "net/http/httptest" + "strings" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +// go test -run Test_Proxy_Empty_Host +func Test_Proxy_Empty_Host(t *testing.T) { + app := fiber.New(fiber.Config{ + DisableStartupMessage: true, + }) + app.Use(New( + Config{Hosts: ""}, + )) + + resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusNotFound, resp.StatusCode) +} + +// go test -run Test_Proxy_Next +func Test_Proxy_Next(t *testing.T) { + app := fiber.New(fiber.Config{ + DisableStartupMessage: true, + }) + app.Use(New(Config{ + Hosts: "next", + Next: func(_ *fiber.Ctx) bool { + return true + }, + })) + + resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusNotFound, resp.StatusCode) +} + +// go test -run Test_Proxy +func Test_Proxy(t *testing.T) { + target := fiber.New(fiber.Config{ + DisableStartupMessage: true, + }) + + target.Get("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusTeapot) + }) + + go func() { + utils.AssertEqual(t, nil, target.Listen(":3001")) + }() + + resp, err := target.Test(httptest.NewRequest("GET", "/", nil), 2000) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusTeapot, resp.StatusCode) + + app := fiber.New(fiber.Config{ + DisableStartupMessage: true, + }) + + host := "localhost:3001" + + app.Use(New(Config{ + Hosts: host, + })) + + req := httptest.NewRequest("GET", "/", nil) + req.Host = host + resp, err = app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusTeapot, resp.StatusCode) +} + +// go test -run Test_Proxy_Before_With_Error +func Test_Proxy_Before_With_Error(t *testing.T) { + app := fiber.New(fiber.Config{ + DisableStartupMessage: true, + }) + + errStr := "error after Before" + + app.Use( + New(Config{ + Hosts: "host", + Before: func(c *fiber.Ctx) error { + return fmt.Errorf(errStr) + }, + })) + + resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusInternalServerError, resp.StatusCode) + + b, err := ioutil.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, errStr, string(b)) +} + +// go test -run Test_Proxy_After_With_Error +func Test_Proxy_After_With_Error(t *testing.T) { + target := fiber.New(fiber.Config{ + DisableStartupMessage: true, + }) + + target.Get("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusTeapot) + }) + + go func() { + utils.AssertEqual(t, nil, target.Listen(":3002")) + }() + + resp, err := target.Test(httptest.NewRequest("GET", "/", nil), 2000) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusTeapot, resp.StatusCode) + + app := fiber.New(fiber.Config{ + DisableStartupMessage: true, + }) + + host := "localhost:3001" + errStr := "error after After" + + app.Use(New(Config{ + Hosts: host, + After: func(ctx *fiber.Ctx) error { + utils.AssertEqual(t, fiber.StatusTeapot, ctx.Response().StatusCode()) + return fmt.Errorf(errStr) + }, + })) + + req := httptest.NewRequest("GET", "/", nil) + req.Host = host + resp, err = app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusInternalServerError, resp.StatusCode) + + b, err := ioutil.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, errStr, string(b)) +} + +// go test -run Test_Proxy_Do_With_Error +func Test_Proxy_Do_With_Error(t *testing.T) { + app := fiber.New(fiber.Config{ + DisableStartupMessage: true, + }) + + app.Use( + New(Config{ + Hosts: "localhost:90000", + })) + + resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusInternalServerError, resp.StatusCode) + + b, err := ioutil.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, strings.Contains(string(b), "127.0.0.1:90000")) +} diff --git a/middleware/recover.go b/middleware/recover.go deleted file mode 100644 index 2ea24b33..00000000 --- a/middleware/recover.go +++ /dev/null @@ -1,24 +0,0 @@ -package middleware - -import ( - "fmt" - - "github.com/gofiber/fiber" -) - -// Recover will recover from panics and calls the ErrorHandler -func Recover() fiber.Handler { - return func(ctx *fiber.Ctx) { - defer func() { - if r := recover(); r != nil { - err, ok := r.(error) - if !ok { - err = fmt.Errorf("%v", r) - } - ctx.Next(err) - return - } - }() - ctx.Next() - } -} diff --git a/middleware/recover.md b/middleware/recover.md deleted file mode 100644 index 127a9642..00000000 --- a/middleware/recover.md +++ /dev/null @@ -1,28 +0,0 @@ -# Recover -Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized [ErrorHandler](https://docs.gofiber.io/error-handling). - -### Example -Import the middleware package that is part of the Fiber web framework -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) -``` - -After you initiate your Fiber app, you can use the following possibilities: -```go -func main() { - app := fiber.New() - - // Default recover - app.Use(middleware.Recover()) - - // ... -} -``` - -### Signatures -```go -func Recover() fiber.Handler {} -``` \ No newline at end of file diff --git a/middleware/recover/README.md b/middleware/recover/README.md new file mode 100644 index 00000000..e9aec955 --- /dev/null +++ b/middleware/recover/README.md @@ -0,0 +1,52 @@ +# Recover +Recover middleware for [Fiber](https://github.com/gofiber/fiber) that recovers from panics anywhere in the stack chain and handles the control to the centralized [ErrorHandler](https://docs.gofiber.io/error-handling). + +### Table of Contents +- [Signatures](#signatures) +- [Examples](#examples) +- [Config](#config) +- [Default Config](#default-config) + + +### Signatures +```go +func New(config ...Config) fiber.Handler +``` + +### Examples +Import the middleware package that is part of the Fiber web framework +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: +```go +// Default middleware config +app.Use(recover.New()) + +// This panic will be catch by the middleware +app.Get("/", func(c *fiber.Ctx) error { + panic("I'm an error") +}) +``` + +### Config +```go +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool +} +``` + +### Default Config +```go +var ConfigDefault = Config{ + Next: nil, +} +``` \ No newline at end of file diff --git a/middleware/recover/recover.go b/middleware/recover/recover.go new file mode 100644 index 00000000..fe3ea5e2 --- /dev/null +++ b/middleware/recover/recover.go @@ -0,0 +1,58 @@ +package recover + +import ( + "fmt" + + "github.com/gofiber/fiber/v2" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Next: nil, +} + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + // Set default config + cfg := ConfigDefault + + // Override config if provided + if len(config) > 0 { + cfg = config[0] + + // Set default values + if cfg.Next == nil { + cfg.Next = ConfigDefault.Next + } + } + + // Return new handler + return func(c *fiber.Ctx) (err error) { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Catch panics + defer func() { + if r := recover(); r != nil { + var ok bool + if err, ok = r.(error); !ok { + // Set error that will call the global error handler + err = fmt.Errorf("%v", r) + } + } + }() + + // Return err if exist, else move to next handler + return c.Next() + } +} diff --git a/middleware/recover/recover_test.go b/middleware/recover/recover_test.go new file mode 100644 index 00000000..05e7c34a --- /dev/null +++ b/middleware/recover/recover_test.go @@ -0,0 +1,29 @@ +package recover + +import ( + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +// go test -run Test_Recover +func Test_Recover(t *testing.T) { + app := fiber.New(fiber.Config{ + ErrorHandler: func(c *fiber.Ctx, err error) error { + utils.AssertEqual(t, "Hi, I'm an error!", err.Error()) + return c.SendStatus(fiber.StatusTeapot) + }, + }) + + app.Use(New()) + + app.Get("/panic", func(c *fiber.Ctx) error { + panic("Hi, I'm an error!") + }) + + resp, err := app.Test(httptest.NewRequest("GET", "/panic", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusTeapot, resp.StatusCode) +} diff --git a/middleware/recover_test.go b/middleware/recover_test.go deleted file mode 100644 index 0cbd5836..00000000 --- a/middleware/recover_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package middleware - -import ( - "io/ioutil" - "net/http/httptest" - "testing" - - "github.com/gofiber/fiber" - "github.com/gofiber/utils" - "github.com/valyala/fasthttp" -) - -// go test -run Test_Middleware_Recover -func Test_Middleware_Recover(t *testing.T) { - app := fiber.New() - - app.Use(Recover()) - - app.Get("/panic", func(ctx *fiber.Ctx) { - ctx.Set("dummy", "this should be here") - panic("Hi, I'm an error!") - }) - - resp, err := app.Test(httptest.NewRequest("GET", "/panic", nil)) - utils.AssertEqual(t, nil, err, "app.Test(req)") - utils.AssertEqual(t, 500, resp.StatusCode, "Status code") - utils.AssertEqual(t, "this should be here", resp.Header.Get("dummy")) - - body, err := ioutil.ReadAll(resp.Body) - utils.AssertEqual(t, nil, err) - utils.AssertEqual(t, "Hi, I'm an error!", string(body)) -} - -// go test -v -run=^$ -bench=Benchmark_Middleware_Recover -benchmem -count=4 -func Benchmark_Middleware_Recover(b *testing.B) { - - app := fiber.New() - app.Use(Recover()) - - app.Get("/", func(c *fiber.Ctx) {}) - handler := app.Handler() - - c := &fasthttp.RequestCtx{} - c.Request.SetRequestURI("/") - - b.ReportAllocs() - b.ResetTimer() - for n := 0; n < b.N; n++ { - handler(c) - } -} diff --git a/middleware/request_id.go b/middleware/request_id.go deleted file mode 100644 index e5484d1d..00000000 --- a/middleware/request_id.go +++ /dev/null @@ -1,96 +0,0 @@ -package middleware - -import ( - fiber "github.com/gofiber/fiber" - utils "github.com/gofiber/utils" -) - -// Middleware types -type ( - // RequestIDConfig defines the config for Logger middleware. - RequestIDConfig struct { - // Next defines a function to skip this middleware. - Next func(ctx *fiber.Ctx) bool - - // Header is the header key where to get/set the unique ID - // Optional. Default: X-Request-ID - Header string - - // Generator defines a function to generate the unique identifier. - // Optional. Default: func() string { - // return utils.UUID() - // } - Generator func() string - } -) - -// RequestIDConfigDefault is the default config -var RequestIDConfigDefault = RequestIDConfig{ - Next: nil, - Header: fiber.HeaderXRequestID, - Generator: func() string { - return utils.UUID() - }, -} - -// RequestID adds an UUID indentifier to the request -/* -RequestID adds an UUID indentifier to the request, the following config arguments in any order: - - RequestID() - - RequestID(next func(*fiber.Ctx) bool) - - RequestID(header string) - - RequestID(generator func() string) - - RequestID(config RequestIDConfig) -*/ -func RequestID(options ...interface{}) fiber.Handler { - // Create default config - var config = RequestIDConfigDefault - // Assert options if provided to adjust the config - if len(options) > 0 { - for i := range options { - switch opt := options[i].(type) { - case func(*fiber.Ctx) bool: - config.Next = opt - case string: - config.Header = opt - case func() string: - config.Generator = opt - case RequestIDConfig: - config = opt - default: - panic("RequestID: the following option types are allowed: `string`, `func() string`, `func(*fiber.Ctx) bool`, `RequestIDConfig`") - } - } - } - // Return requestID - return requestID(config) -} - -func requestID(config RequestIDConfig) fiber.Handler { - // Set default values - if config.Header == "" { - config.Header = RequestIDConfigDefault.Header - } - if config.Generator == nil { - config.Generator = RequestIDConfigDefault.Generator - } - - // Return handler - return func(ctx *fiber.Ctx) { - // Don't execute the middleware if Next returns true - if config.Next != nil && config.Next(ctx) { - ctx.Next() - return - } - // Get id from request - rid := ctx.Get(config.Header) - // Create new UUID if empty - if len(rid) <= 0 { - rid = config.Generator() - } - // Set new id to response header - ctx.Set(config.Header, rid) - // Continue stack - ctx.Next() - } -} diff --git a/middleware/request_id.md b/middleware/request_id.md deleted file mode 100644 index b8b51668..00000000 --- a/middleware/request_id.md +++ /dev/null @@ -1,65 +0,0 @@ -# RequestID -Adds an indentifier to the response using the `X-Request-ID` header - -### Example -Import the middleware package that is part of the Fiber web framework -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) -``` - -After you initiate your Fiber app, you can use the following possibilities: -```go -func main() { - app := fiber.New() - - // Default RequestID - app.Use(middleware.RequestID()) - - // Custom Header - app.Use(middleware.RequestID("X-Custom-Header")) - - // Custom ID generator - app.Use(middleware.RequestID(func() string { - return "1234567890" - })) - - // Custom Config - app.Use(middleware.RequestID(middleware.RequestIDConfig{ - Next: func(ctx *fiber.Ctx) bool { - return ctx.Method() != fiber.MethodPost - }, - Header: "X-Custom-Header", - Generator: func() string { - return "1234567890" - }, - })) - - // ... -} -``` - -### Signatures -```go -func RequestID(options ...interface{}) fiber.Handler {} -``` - -### Config -```go -type RequestIDConfig struct { - // Next defines a function to skip this middleware. - Next func(ctx *fiber.Ctx) bool - - // Header is the header key where to get/set the unique ID - // Optiona. Defaults: X-Request-ID - Header string - - // Generator defines a function to generate the unique identifier. - // Optional. Default: func() string { - // return utils.UUID() - // } - Generator func() string -} -``` \ No newline at end of file diff --git a/middleware/request_id_test.go b/middleware/request_id_test.go deleted file mode 100644 index 905c62fa..00000000 --- a/middleware/request_id_test.go +++ /dev/null @@ -1,184 +0,0 @@ -package middleware - -import ( - "fmt" - "net/http" - "net/http/httptest" - "testing" - - "github.com/gofiber/fiber" - "github.com/gofiber/utils" - "github.com/valyala/fasthttp" -) - -var ( - UUIDLen = 36 -) - -// go test -run Test_Middleware_RequestID -func Test_Middleware_RequestID(t *testing.T) { - app := fiber.New() - - app.Use(RequestID()) - - app.Get("/", func(ctx *fiber.Ctx) { - ctx.Send("Hello?") - }) - - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) - utils.AssertEqual(t, nil, err, "app.Test(req)") - utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") - reqid := resp.Header.Get(fiber.HeaderXRequestID) - utils.AssertEqual(t, UUIDLen, len(reqid)) - - req := httptest.NewRequest("GET", "/", nil) - req.Header.Add(fiber.HeaderXRequestID, reqid) - - resp, err = app.Test(req) - utils.AssertEqual(t, nil, err, "app.Test(req)") - utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") - utils.AssertEqual(t, reqid, resp.Header.Get(fiber.HeaderXRequestID)) -} - -// go test -run Test_Middleware_RequestID_Header -func Test_Middleware_RequestID_Header(t *testing.T) { - app := fiber.New() - - app.Use(RequestID("X-Test-header")) - - app.Get("/", func(ctx *fiber.Ctx) { - ctx.Send("Hello?") - }) - - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) - utils.AssertEqual(t, nil, err, "app.Test(req)") - utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") - reqid := resp.Header.Get("X-Test-header") - utils.AssertEqual(t, UUIDLen, len(reqid)) - - req := httptest.NewRequest("GET", "/", nil) - req.Header.Add("X-Test-header", reqid) - - resp, err = app.Test(req) - utils.AssertEqual(t, nil, err, "app.Test(req)") - utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") - utils.AssertEqual(t, reqid, resp.Header.Get("X-Test-header")) -} - -// go test -run Test_Middleware_RequestID_Options_And_WithConfig -func Test_Middleware_RequestID_Options_And_WithConfig(t *testing.T) { - - testCases := []struct { - idLen int - header string - handler fiber.Handler - }{ - { - idLen: UUIDLen, - header: "X-Test-header", - handler: RequestID("X-Test-header"), - }, - { - idLen: 7, - header: RequestIDConfigDefault.Header, - handler: RequestID(func() string { return "fake-id" }), - }, - { - idLen: UUIDLen, - header: RequestIDConfigDefault.Header, - handler: RequestID(RequestIDConfig{}), - }, - } - - for _, testCase := range testCases { - app := fiber.New() - - app.Use(testCase.handler) - - app.Get("/", func(ctx *fiber.Ctx) { - ctx.Send("Hello?") - }) - - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) - utils.AssertEqual(t, nil, err, "app.Test(req)") - utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") - reqid := resp.Header.Get(testCase.header) - utils.AssertEqual(t, testCase.idLen, len(reqid)) - } -} - -// go test -run Test_Middleware_RequestID_Config -func Test_Middleware_RequestID_Config(t *testing.T) { - app := fiber.New() - - app.Use(RequestID(RequestIDConfig{ - Header: "X-Test-Header", - Generator: func() string { - return "johndoe" - }, - })) - - app.Get("/", func(ctx *fiber.Ctx) { - ctx.Send("Hello?") - }) - - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) - utils.AssertEqual(t, nil, err, "app.Test(req)") - utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") - reqid := resp.Header.Get("X-Test-Header") - utils.AssertEqual(t, "johndoe", reqid) - - req := httptest.NewRequest("GET", "/", nil) - req.Header.Add(fiber.HeaderXRequestID, reqid) - - resp, err = app.Test(req) - utils.AssertEqual(t, nil, err, "app.Test(req)") - utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") - utils.AssertEqual(t, reqid, resp.Header.Get("X-Test-Header")) -} - -// go test -run Test_Middleware_RequestID_Skip -func Test_Middleware_RequestID_Skip(t *testing.T) { - app := fiber.New() - - app.Use(RequestID(func(_ *fiber.Ctx) bool { - return true - })) - - app.Get("/", func(ctx *fiber.Ctx) {}) - - resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/", nil)) - utils.AssertEqual(t, nil, err, "app.Test(req)") - utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") - utils.AssertEqual(t, "", resp.Header.Get(RequestIDConfigDefault.Header), RequestIDConfigDefault.Header) -} - -// go test -run Test_Middleware_RequestID_Panic -func Test_Middleware_RequestID_Panic(t *testing.T) { - defer func() { - utils.AssertEqual(t, - "RequestID: the following option types are allowed: `string`, `func() string`, `func(*fiber.Ctx) bool`, `RequestIDConfig`", - fmt.Sprintf("%s", recover())) - }() - - RequestID(0) -} - -// go test -v -run=^$ -bench=Benchmark_Middleware_RequestID -benchmem -count=4 -func Benchmark_Middleware_RequestID(b *testing.B) { - - app := fiber.New() - app.Use(RequestID()) - - app.Get("/", func(c *fiber.Ctx) {}) - handler := app.Handler() - - c := &fasthttp.RequestCtx{} - c.Request.SetRequestURI("/") - - b.ReportAllocs() - b.ResetTimer() - for n := 0; n < b.N; n++ { - handler(c) - } -} diff --git a/middleware/requestid/README.md b/middleware/requestid/README.md new file mode 100644 index 00000000..80269796 --- /dev/null +++ b/middleware/requestid/README.md @@ -0,0 +1,72 @@ +# RequestID +RequestID middleware for [Fiber](https://github.com/gofiber/fiber) that adds an indentifier to the response. + +### Table of Contents +- [Signatures](#signatures) +- [Examples](#examples) +- [Config](#config) +- [Default Config](#default-config) + + +### Signatures +```go +func New(config ...Config) fiber.Handler +``` + +### Examples +Import the middleware package that is part of the Fiber web framework +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/requestid" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: +```go +// Default middleware config +app.Use(requestid.New()) + +// Or extend your config for customization +app.Use(requestid.New(requestid.Config{ + Header: "X-Custom-Header", + Generetor: func() string { + return "static-id" + } +})) +``` + +### Config +```go +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Header is the header key where to get/set the unique request ID + // + // Optional. Default: "X-Request-ID" + Header string + + // Generator defines a function to generate the unique identifier. + // + // Optional. Default: func() string { + // return utils.UUID() + // } + Generator func() string +} +``` + +### Default Config +```go +var ConfigDefault = Config{ + Next: nil, + Header: fiber.HeaderXRequestID, + Generator: func() string { + return utils.UUID() + }, +} + +``` \ No newline at end of file diff --git a/middleware/requestid/requestid.go b/middleware/requestid/requestid.go new file mode 100644 index 00000000..68f6faff --- /dev/null +++ b/middleware/requestid/requestid.go @@ -0,0 +1,73 @@ +package requestid + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Header is the header key where to get/set the unique request ID + // + // Optional. Default: "X-Request-ID" + Header string + + // Generator defines a function to generate the unique identifier. + // + // Optional. Default: func() string { + // return utils.UUID() + // } + Generator func() string +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Next: nil, + Header: fiber.HeaderXRequestID, + Generator: func() string { + return utils.UUID() + }, +} + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + // Set default config + cfg := ConfigDefault + + // Override config if provided + if len(config) > 0 { + cfg = config[0] + + // Set default values + if cfg.Next == nil { + cfg.Next = ConfigDefault.Next + } + if cfg.Header == "" { + cfg.Header = ConfigDefault.Header + } + if cfg.Generator == nil { + cfg.Generator = ConfigDefault.Generator + } + } + + // Return new handler + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + // Get id from request, else we generate one + rid := c.Get(cfg.Header, cfg.Generator()) + + // Set new id to response header + c.Set(cfg.Header, rid) + + // Continue stack + return c.Next() + } +} diff --git a/middleware/requestid/requestid_test.go b/middleware/requestid/requestid_test.go new file mode 100644 index 00000000..84db2d19 --- /dev/null +++ b/middleware/requestid/requestid_test.go @@ -0,0 +1,35 @@ +package requestid + +import ( + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +// go test -run Test_RequestID +func Test_RequestID(t *testing.T) { + app := fiber.New() + + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World ๐Ÿ‘‹!") + }) + + resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + + reqid := resp.Header.Get(fiber.HeaderXRequestID) + utils.AssertEqual(t, 36, len(reqid)) + + req := httptest.NewRequest("GET", "/", nil) + req.Header.Add(fiber.HeaderXRequestID, reqid) + + resp, err = app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + utils.AssertEqual(t, reqid, resp.Header.Get(fiber.HeaderXRequestID)) +} diff --git a/middleware/testdata/compress_level_-1.txt b/middleware/testdata/compress_level_-1.txt deleted file mode 100644 index 5bc10dab..00000000 --- a/middleware/testdata/compress_level_-1.txt +++ /dev/null @@ -1,20 +0,0 @@ -compress_level_-1.txt -compress_level_-1.txt -compress_level_-1.txt -compress_level_-1.txt -compress_level_-1.txt -compress_level_-1.txt -compress_level_-1.txt -compress_level_-1.txt -compress_level_-1.txt -compress_level_-1.txt -compress_level_-1.txt -compress_level_-1.txt -compress_level_-1.txt -compress_level_-1.txt -compress_level_-1.txt -compress_level_-1.txt -compress_level_-1.txt -compress_level_-1.txt -compress_level_-1.txt -compress_level_-1.txt diff --git a/middleware/testdata/compress_level_0.txt b/middleware/testdata/compress_level_0.txt deleted file mode 100644 index 947959f3..00000000 --- a/middleware/testdata/compress_level_0.txt +++ /dev/null @@ -1,20 +0,0 @@ -compress_level_0.txt -compress_level_0.txt -compress_level_0.txt -compress_level_0.txt -compress_level_0.txt -compress_level_0.txt -compress_level_0.txt -compress_level_0.txt -compress_level_0.txt -compress_level_0.txt -compress_level_0.txt -compress_level_0.txt -compress_level_0.txt -compress_level_0.txt -compress_level_0.txt -compress_level_0.txt -compress_level_0.txt -compress_level_0.txt -compress_level_0.txt -compress_level_0.txt diff --git a/middleware/testdata/compress_level_1.txt b/middleware/testdata/compress_level_1.txt deleted file mode 100644 index 36dd13e1..00000000 --- a/middleware/testdata/compress_level_1.txt +++ /dev/null @@ -1,20 +0,0 @@ -compress_level_1.txt -compress_level_1.txt -compress_level_1.txt -compress_level_1.txt -compress_level_1.txt -compress_level_1.txt -compress_level_1.txt -compress_level_1.txt -compress_level_1.txt -compress_level_1.txt -compress_level_1.txt -compress_level_1.txt -compress_level_1.txt -compress_level_1.txt -compress_level_1.txt -compress_level_1.txt -compress_level_1.txt -compress_level_1.txt -compress_level_1.txt -compress_level_1.txt diff --git a/middleware/testdata/compress_level_2.txt b/middleware/testdata/compress_level_2.txt deleted file mode 100644 index a0c1b6ad..00000000 --- a/middleware/testdata/compress_level_2.txt +++ /dev/null @@ -1,20 +0,0 @@ -compress_level_2.txt -compress_level_2.txt -compress_level_2.txt -compress_level_2.txt -compress_level_2.txt -compress_level_2.txt -compress_level_2.txt -compress_level_2.txt -compress_level_2.txt -compress_level_2.txt -compress_level_2.txt -compress_level_2.txt -compress_level_2.txt -compress_level_2.txt -compress_level_2.txt -compress_level_2.txt -compress_level_2.txt -compress_level_2.txt -compress_level_2.txt -compress_level_2.txt diff --git a/middleware/testdata/favicon.ico b/middleware/testdata/favicon.ico deleted file mode 100644 index b752c266..00000000 Binary files a/middleware/testdata/favicon.ico and /dev/null differ diff --git a/middleware/timeout.md b/middleware/timeout.md deleted file mode 100644 index e4b1666e..00000000 --- a/middleware/timeout.md +++ /dev/null @@ -1,34 +0,0 @@ -# Timeout -Wrapper function which provides a handler with a timeout. - -If the handler takes longer than the given duration to return, the timeout error is set and forwarded to the error handler. - -### Example -Import the middleware package that is part of the Fiber web framework -```go -import ( - "github.com/gofiber/fiber" - "github.com/gofiber/fiber/middleware" -) -``` - -After you initiate your Fiber app, you can use the following possibilities: -```go -func main() { - app := fiber.New() - - handler := func(ctx *fiber.Ctx) { - ctx.Send("Hello, World ๐Ÿ‘‹!") - } - - // Wrap the handler with a timeout - app.Get("/foo", middleware.Timeout(handler, 5 * time.Second)) - - // ... -} -``` - -### Signatures -```go -func Timeout(handler fiber.Handler, timeout time.Duration) fiber.Handler {} -``` diff --git a/middleware/timeout/README.md b/middleware/timeout/README.md new file mode 100644 index 00000000..2687faa1 --- /dev/null +++ b/middleware/timeout/README.md @@ -0,0 +1,30 @@ +# Timeout +Timeout middleware for [Fiber](https://github.com/gofiber/fiber) wraps a `fiber.Handler` with a timeout. If the handler takes longer than the given duration to return, the timeout error is set and forwarded to the centralized [ErrorHandler](https://docs.gofiber.io/error-handling). + +### Table of Contents +- [Signatures](#signatures) +- [Examples](#examples) + + +### Signatures +```go +func New(h fiber.Handler, t time.Duration) fiber.Handler +``` + +### Examples +Import the middleware package that is part of the Fiber web framework +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/timeout" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: +```go +handler := func(ctx *fiber.Ctx) { + ctx.Send("Hello, World ๐Ÿ‘‹!") +} + +app.Get("/foo", timeout.New(handler, 5 * time.Second)) +``` \ No newline at end of file diff --git a/middleware/timeout.go b/middleware/timeout/timeout.go similarity index 53% rename from middleware/timeout.go rename to middleware/timeout/timeout.go index b7cfdfe2..6dceeee5 100644 --- a/middleware/timeout.go +++ b/middleware/timeout/timeout.go @@ -1,33 +1,35 @@ -package middleware +package timeout import ( "time" - fiber "github.com/gofiber/fiber" + "github.com/gofiber/fiber/v2" ) -// Timeout wraps a handler and aborts the process of the handler if the timeout is reached -func Timeout(handler fiber.Handler, timeout time.Duration) fiber.Handler { +// New wraps a handler and aborts the process of the handler if the timeout is reached +func New(handler fiber.Handler, timeout time.Duration) fiber.Handler { if timeout <= 0 { return handler } // logic is from fasthttp.TimeoutWithCodeHandler https://github.com/valyala/fasthttp/blob/master/server.go#L418 - return func(ctx *fiber.Ctx) { + return func(ctx *fiber.Ctx) error { ch := make(chan struct{}, 1) go func() { defer func() { _ = recover() }() - handler(ctx) + _ = handler(ctx) ch <- struct{}{} }() select { case <-ch: case <-time.After(timeout): - ctx.Next(fiber.ErrRequestTimeout) + return fiber.ErrRequestTimeout } + + return nil } } diff --git a/middleware/timeout_test.go b/middleware/timeout/timeout_test.go similarity index 62% rename from middleware/timeout_test.go rename to middleware/timeout/timeout_test.go index 7df087b4..dc60e2eb 100644 --- a/middleware/timeout_test.go +++ b/middleware/timeout/timeout_test.go @@ -1,17 +1,14 @@ -package middleware +package timeout -// go test -run Test_Middleware_Timeout +// // go test -run Test_Middleware_Timeout // func Test_Middleware_Timeout(t *testing.T) { -// app := fiber.New(&fiber.Settings{DisableStartupMessage: true}) +// app := fiber.New(fiber.Config{DisableStartupMessage: true}) -// h := Timeout( -// func(c *fiber.Ctx) { -// sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") -// time.Sleep(sleepTime) -// c.SendString("After " + c.Params("sleepTime") + "ms sleeping") -// }, -// 5*time.Millisecond, -// ) +// h := New(func(c *fiber.Ctx) error { +// sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") +// time.Sleep(sleepTime) +// return c.SendString("After " + c.Params("sleepTime") + "ms sleeping") +// }, 5*time.Millisecond) // app.Get("/test/:sleepTime", h) // testTimeout := func(timeoutStr string) { @@ -39,23 +36,18 @@ package middleware // testSucces("3") // } -// go test -run Test_Middleware_Timeout_Panic -// func Test_Middleware_Timeout_Panic(t *testing.T) { -// app := fiber.New(&fiber.Settings{DisableStartupMessage: true}) +// // go test -run -v Test_Timeout_Panic +// func Test_Timeout_Panic(t *testing.T) { +// app := fiber.New(fiber.Config{DisableStartupMessage: true}) -// h := Timeout( -// func(c *fiber.Ctx) { -// c.Set("dummy", "this should not be here") -// panic("panic in timeout handler") -// }, -// 5*time.Millisecond, -// ) -// app.Get("/panic", Recover(), h) +// app.Get("/panic", recover.New(), New(func(c *fiber.Ctx) error { +// c.Set("dummy", "this should not be here") +// panic("panic in timeout handler") +// }, 5*time.Millisecond)) // resp, err := app.Test(httptest.NewRequest("GET", "/panic", nil)) // utils.AssertEqual(t, nil, err, "app.Test(req)") // utils.AssertEqual(t, fiber.StatusRequestTimeout, resp.StatusCode, "Status code") -// utils.AssertEqual(t, "", resp.Header.Get("dummy")) // body, err := ioutil.ReadAll(resp.Body) // utils.AssertEqual(t, nil, err) diff --git a/path.go b/path.go index 133cea9f..d8a15e70 100644 --- a/path.go +++ b/path.go @@ -9,17 +9,16 @@ package fiber import ( "strconv" "strings" - "sync/atomic" - utils "github.com/gofiber/utils" + "github.com/gofiber/fiber/v2/utils" ) // routeParser holds the path segments and param names type routeParser struct { - segs []routeSegment // the parsed segments of the route - params []string // that parameter names the parsed route - wildCardCount int // number of wildcard parameters, used internally to give the wildcard parameter its number - plusCount int // number of plus parameters, used internally to give the plus parameter its number + segs []*routeSegment // the parsed segments of the route + params []string // that parameter names the parsed route + wildCardCount int // number of wildcard parameters, used internally to give the wildcard parameter its number + plusCount int // number of plus parameters, used internally to give the plus parameter its number } // paramsSeg holds the segment metadata @@ -34,7 +33,9 @@ type routeSegment struct { IsGreedy bool // indicates whether the parameter is greedy or not, is used with wildcard and plus IsOptional bool // indicates whether the parameter is optional or not // common information - IsLast bool // shows if the segment is the last one for the route + IsLast bool // shows if the segment is the last one for the route + HasOptionalSlash bool // segment has the possibility of an optional slash + Length int // length of the parameter for segment, when its 0 then the length is undetermined // future TODO: add support for optional groups "/abc(/def)?" } @@ -93,10 +94,11 @@ func parseRoute(pattern string) routeParser { // addParameterMetaInfo add important meta information to the parameter segments // to simplify the search for the end of the parameter -func addParameterMetaInfo(segs []routeSegment) []routeSegment { +func addParameterMetaInfo(segs []*routeSegment) []*routeSegment { comparePart := "" + segLen := len(segs) // loop from end to begin - for i := len(segs) - 1; i >= 0; i-- { + for i := segLen - 1; i >= 0; i-- { // set the compare part for the parameter if segs[i].IsParam { // important for finding the end of the parameter @@ -110,15 +112,26 @@ func addParameterMetaInfo(segs []routeSegment) []routeSegment { } // loop from begin to end - for i := 0; i < len(segs); i++ { + for i := 0; i < segLen; i++ { // check how often the compare part is in the following const parts - if segs[i].IsParam && segs[i].ComparePart != "" { + if segs[i].IsParam { + // check if parameter segments are directly after each other and if one of them is greedy + // in case the next parameter or the current parameter is not a wildcard its not greedy, we only want one character + if segLen > i+1 && !segs[i].IsGreedy && segs[i+1].IsParam && !segs[i+1].IsGreedy { + segs[i].Length = 1 + } + if segs[i].ComparePart == "" { + continue + } for j := i + 1; j <= len(segs)-1; j++ { if !segs[j].IsParam { // count is important for the greedy match segs[i].PartCount += strings.Count(segs[j].Const, segs[i].ComparePart) } } + // check if the end of the segment is a optional slash and then if the segement is optional or the last one + } else if segs[i].Const[len(segs[i].Const)-1] == slashDelimiter && (segs[i].IsLast || (segLen > i+1 && segs[i+1].IsOptional)) { + segs[i].HasOptionalSlash = true } } @@ -143,20 +156,21 @@ func findNextParamPosition(pattern string) int { } // analyseConstantPart find the end of the constant part and create the route segment -func (routeParser *routeParser) analyseConstantPart(pattern string, nextParamPosition int) (string, routeSegment) { +func (routeParser *routeParser) analyseConstantPart(pattern string, nextParamPosition int) (string, *routeSegment) { // handle the constant part processedPart := pattern if nextParamPosition != -1 { // remove the constant part until the parameter processedPart = pattern[:nextParamPosition] } - return processedPart, routeSegment{ - Const: processedPart, + return processedPart, &routeSegment{ + Const: processedPart, + Length: len(processedPart), } } // analyseParameterPart find the parameter end and create the route segment -func (routeParser *routeParser) analyseParameterPart(pattern string) (string, routeSegment) { +func (routeParser *routeParser) analyseParameterPart(pattern string) (string, *routeSegment) { isWildCard := pattern[0] == wildcardParam isPlusParam := pattern[0] == plusParam parameterEndPosition := findNextCharsetPosition(pattern[1:], parameterEndChars) @@ -182,7 +196,7 @@ func (routeParser *routeParser) analyseParameterPart(pattern string) (string, ro paramName += strconv.Itoa(routeParser.plusCount) } - return processedPart, routeSegment{ + return processedPart, &routeSegment{ ParamName: paramName, IsParam: true, IsOptional: isWildCard || pattern[parameterEndPosition] == optionalParam, @@ -213,93 +227,66 @@ func findNextCharsetPosition(search string, charset []byte) int { } // getMatch parses the passed url and tries to match it against the route segments and determine the parameter positions -func (routeParser *routeParser) getMatch(s string, partialCheck bool) ([][2]int, bool) { - lenKeys := len(routeParser.params) - paramsPositions := getAllocFreeParamsPos(lenKeys) - var i, paramsIterator, partLen, paramStart int - for index, segment := range routeParser.segs { +func (routeParser *routeParser) getMatch(s, original string, params *[maxParams]string, partialCheck bool) bool { + var i, paramsIterator, partLen int + for _, segment := range routeParser.segs { partLen = len(s) // check const segment if !segment.IsParam { - optionalPart := false - i = len(segment.Const) - // check if the end of the segment is a optional slash and then if the segement is optional or the last one - if i > 0 && partLen == i-1 && segment.Const[i-1] == slashDelimiter && s[:i-1] == segment.Const[:i-1] { - if segment.IsLast || routeParser.segs[index+1].IsOptional { - i-- - optionalPart = true - } - } + i = segment.Length // is optional part or the const part must match with the given string - if !optionalPart && (partLen < i || (i == 0 && partLen > 0) || s[:i] != segment.Const) { - return nil, false + // check if the end of the segment is a optional slash + if segment.HasOptionalSlash && partLen == i-1 && s == segment.Const[:i-1] { + i-- + } else if !(i <= partLen && s[:i] == segment.Const) { + return false } } else { // determine parameter length - i = findParamLen(s, routeParser.segs, index) + i = findParamLen(s, segment) if !segment.IsOptional && i == 0 { - return nil, false + return false } // take over the params positions - paramsPositions[paramsIterator][0], paramsPositions[paramsIterator][1] = paramStart, paramStart+i + params[paramsIterator] = original[:i] paramsIterator++ } // reduce founded part from the string if partLen > 0 { - if partLen < i { - i = partLen - } - paramStart += i - - s = s[i:] + s, original = s[i:], original[i:] } } if len(s) != 0 && !partialCheck { - return nil, false + return false } - return paramsPositions, true -} - -// paramsForPos get parameters for the given positions from the given path -func (routeParser *routeParser) paramsForPos(path string, paramsPositions [][2]int) []string { - size := len(paramsPositions) - params := getAllocFreeParams(size) - for i, positions := range paramsPositions { - if positions[0] != positions[1] && len(path) >= positions[1] { - params[i] = path[positions[0]:positions[1]] - } else { - params[i] = "" - } - } - - return params + return true } // findParamLen for the expressjs wildcard behavior (right to left greedy) // look at the other segments and take what is left for the wildcard from right to left -func findParamLen(s string, segments []routeSegment, currIndex int) int { - if segments[currIndex].IsLast { - return findParamLenForLastSegment(s, segments[currIndex]) +func findParamLen(s string, segment *routeSegment) int { + if segment.IsLast { + return findParamLenForLastSegment(s, segment) } - compareSeg := segments[currIndex+1] - // check if parameter segments are directly after each other and if one of them is greedy - if compareSeg.IsParam && !segments[currIndex].IsGreedy && !compareSeg.IsGreedy && len(s) > 0 { - // in case the next parameter or the current parameter is not a wildcard its not greedy, we only want one character - return 1 - } - // Search the parameters until the next constant part - // special logic for greedy params - if segments[currIndex].IsGreedy { - searchCount := strings.Count(s, segments[currIndex].ComparePart) + if segment.Length != 0 && len(s) >= segment.Length { + return segment.Length + } else if segment.IsGreedy { + // Search the parameters until the next constant part + // special logic for greedy params + searchCount := strings.Count(s, segment.ComparePart) if searchCount > 1 { - return findGreedyParamLen(s, searchCount, segments[currIndex]) + return findGreedyParamLen(s, searchCount, segment) } } - if constPosition := strings.Index(s, segments[currIndex].ComparePart); constPosition != -1 { + if len(segment.ComparePart) == 1 { + if constPosition := strings.IndexByte(s, segment.ComparePart[0]); constPosition != -1 { + return constPosition + } + } else if constPosition := strings.Index(s, segment.ComparePart); constPosition != -1 { return constPosition } @@ -307,19 +294,18 @@ func findParamLen(s string, segments []routeSegment, currIndex int) int { } // findParamLenForLastSegment get the length of the parameter if it is the last segment -func findParamLenForLastSegment(s string, seg routeSegment) int { - if seg.IsGreedy { - return len(s) - } - if i := strings.IndexByte(s, slashDelimiter); i != -1 { - return i +func findParamLenForLastSegment(s string, seg *routeSegment) int { + if !seg.IsGreedy { + if i := strings.IndexByte(s, slashDelimiter); i != -1 { + return i + } } return len(s) } // findGreedyParamLen get the length of the parameter for greedy segments from right to left -func findGreedyParamLen(s string, searchCount int, segment routeSegment) int { +func findGreedyParamLen(s string, searchCount int, segment *routeSegment) int { // check all from right to left segments for i := segment.PartCount; i > 0 && searchCount > 0; i-- { searchCount-- @@ -332,39 +318,3 @@ func findGreedyParamLen(s string, searchCount int, segment routeSegment) int { return len(s) } - -// performance tricks -// creates predefined arrays that are used to match the request routes so that no allocations need to be made -var paramsDummy, paramsPosDummy = make([]string, 100000), make([][2]int, 100000) - -// positions parameter that moves further and further to the right and remains atomic over all simultaneous requests -// to assign a separate range to each request -var startParamList, startParamPosList uint32 = 0, 0 - -// getAllocFreeParamsPos fetches a slice area from the predefined slice, which is currently not in use -func getAllocFreeParamsPos(allocLen int) [][2]int { - size := uint32(allocLen) - start := atomic.AddUint32(&startParamPosList, size) - if (start + 10) >= uint32(len(paramsPosDummy)) { - atomic.StoreUint32(&startParamPosList, 0) - return getAllocFreeParamsPos(allocLen) - } - start -= size - allocLen += int(start) - paramsPositions := paramsPosDummy[start:allocLen:allocLen] - return paramsPositions -} - -// getAllocFreeParams fetches a slice area from the predefined slice, which is currently not in use -func getAllocFreeParams(allocLen int) []string { - size := uint32(allocLen) - start := atomic.AddUint32(&startParamList, size) - if (start + 10) >= uint32(len(paramsDummy)) { - atomic.StoreUint32(&startParamList, 0) - return getAllocFreeParams(allocLen) - } - start -= size - allocLen += int(start) - params := paramsDummy[start:allocLen:allocLen] - return params -} diff --git a/path_test.go b/path_test.go index 86b9dea4..8bfcc232 100644 --- a/path_test.go +++ b/path_test.go @@ -6,10 +6,9 @@ package fiber import ( "fmt" - "sync/atomic" "testing" - utils "github.com/gofiber/utils" + "github.com/gofiber/fiber/v2/utils" ) // go test -race -run Test_Path_parseRoute @@ -18,12 +17,12 @@ func Test_Path_parseRoute(t *testing.T) { rp = parseRoute("/shop/product/::filter/color::color/size::size") utils.AssertEqual(t, routeParser{ - segs: []routeSegment{ - {Const: "/shop/product/:"}, + segs: []*routeSegment{ + {Const: "/shop/product/:", Length: 15}, {IsParam: true, ParamName: "filter", ComparePart: "/color:", PartCount: 1}, - {Const: "/color:"}, + {Const: "/color:", Length: 7}, {IsParam: true, ParamName: "color", ComparePart: "/size:", PartCount: 1}, - {Const: "/size:"}, + {Const: "/size:", Length: 6}, {IsParam: true, ParamName: "size", IsLast: true}, }, params: []string{"filter", "color", "size"}, @@ -31,10 +30,10 @@ func Test_Path_parseRoute(t *testing.T) { rp = parseRoute("/api/v1/:param/abc/*") utils.AssertEqual(t, routeParser{ - segs: []routeSegment{ - {Const: "/api/v1/"}, + segs: []*routeSegment{ + {Const: "/api/v1/", Length: 8}, {IsParam: true, ParamName: "param", ComparePart: "/abc", PartCount: 1}, - {Const: "/abc/"}, + {Const: "/abc/", Length: 5, HasOptionalSlash: true}, {IsParam: true, ParamName: "*1", IsGreedy: true, IsOptional: true, IsLast: true}, }, params: []string{"param", "*1"}, @@ -43,12 +42,12 @@ func Test_Path_parseRoute(t *testing.T) { rp = parseRoute("/api/*/:param/:param2") utils.AssertEqual(t, routeParser{ - segs: []routeSegment{ - {Const: "/api/"}, + segs: []*routeSegment{ + {Const: "/api/", Length: 5, HasOptionalSlash: true}, {IsParam: true, ParamName: "*1", IsGreedy: true, IsOptional: true, ComparePart: "/", PartCount: 2}, - {Const: "/"}, + {Const: "/", Length: 1}, {IsParam: true, ParamName: "param", ComparePart: "/", PartCount: 1}, - {Const: "/"}, + {Const: "/", Length: 1}, {IsParam: true, ParamName: "param2", IsLast: true}, }, params: []string{"*1", "param", "param2"}, @@ -57,9 +56,9 @@ func Test_Path_parseRoute(t *testing.T) { rp = parseRoute("/test:optional?:optional2?") utils.AssertEqual(t, routeParser{ - segs: []routeSegment{ - {Const: "/test"}, - {IsParam: true, ParamName: "optional", IsOptional: true}, + segs: []*routeSegment{ + {Const: "/test", Length: 5}, + {IsParam: true, ParamName: "optional", IsOptional: true, Length: 1}, {IsParam: true, ParamName: "optional2", IsOptional: true, IsLast: true}, }, params: []string{"optional", "optional2"}, @@ -67,10 +66,10 @@ func Test_Path_parseRoute(t *testing.T) { rp = parseRoute("/config/+.json") utils.AssertEqual(t, routeParser{ - segs: []routeSegment{ - {Const: "/config/"}, + segs: []*routeSegment{ + {Const: "/config/", Length: 8}, {IsParam: true, ParamName: "+1", IsGreedy: true, IsOptional: false, ComparePart: ".json", PartCount: 1}, - {Const: ".json", IsLast: true}, + {Const: ".json", Length: 5, IsLast: true}, }, params: []string{"+1"}, plusCount: 1, @@ -78,12 +77,12 @@ func Test_Path_parseRoute(t *testing.T) { rp = parseRoute("/api/:day.:month?.:year?") utils.AssertEqual(t, routeParser{ - segs: []routeSegment{ - {Const: "/api/"}, + segs: []*routeSegment{ + {Const: "/api/", Length: 5}, {IsParam: true, ParamName: "day", IsOptional: false, ComparePart: ".", PartCount: 2}, - {Const: "."}, + {Const: ".", Length: 1}, {IsParam: true, ParamName: "month", IsOptional: true, ComparePart: ".", PartCount: 1}, - {Const: "."}, + {Const: ".", Length: 1}, {IsParam: true, ParamName: "year", IsOptional: true, IsLast: true}, }, params: []string{"day", "month", "year"}, @@ -91,12 +90,12 @@ func Test_Path_parseRoute(t *testing.T) { rp = parseRoute("/*v1*/proxy") utils.AssertEqual(t, routeParser{ - segs: []routeSegment{ - {Const: "/"}, + segs: []*routeSegment{ + {Const: "/", Length: 1, HasOptionalSlash: true}, {IsParam: true, ParamName: "*1", IsGreedy: true, IsOptional: true, ComparePart: "v1", PartCount: 1}, - {Const: "v1"}, + {Const: "v1", Length: 2}, {IsParam: true, ParamName: "*2", IsGreedy: true, IsOptional: true, ComparePart: "/proxy", PartCount: 1}, - {Const: "/proxy", IsLast: true}, + {Const: "/proxy", Length: 6, IsLast: true}, }, params: []string{"*1", "*2"}, wildCardCount: 2, @@ -112,15 +111,14 @@ func Test_Path_matchParams(t *testing.T) { match bool partialCheck bool } + var ctxParams [maxParams]string testCase := func(r string, cases []testparams) { parser := parseRoute(r) for _, c := range cases { - paramsPos, match := parser.getMatch(c.url, c.partialCheck) + match := parser.getMatch(c.url, c.url, &ctxParams, c.partialCheck) utils.AssertEqual(t, c.match, match, fmt.Sprintf("route: '%s', url: '%s'", r, c.url)) - if match && paramsPos != nil { - utils.AssertEqual(t, c.params, parser.paramsForPos(c.url, paramsPos), fmt.Sprintf("route: '%s', url: '%s'", r, c.url)) - } else { - utils.AssertEqual(t, true, nil == paramsPos, fmt.Sprintf("route: '%s', url: '%s'", r, c.url)) + if match && len(c.params) > 0 { + utils.AssertEqual(t, c.params[0:len(c.params)-1], ctxParams[0:len(c.params)-1], fmt.Sprintf("route: '%s', url: '%s'", r, c.url)) } } } @@ -396,26 +394,25 @@ func Benchmark_Path_matchParams(t *testing.B) { match bool partialCheck bool } + var ctxParams [maxParams]string benchCase := func(r string, cases []testparams) { parser := parseRoute(r) for _, c := range cases { - - var params []string var matchRes bool - t.Run(r+" | "+c.url, func(b *testing.B) { - params = nil + state := "match" + if !c.match { + state = "not match" + } + t.Run(r+" | "+state+" | "+c.url, func(b *testing.B) { for i := 0; i <= b.N; i++ { - if paramPos, match := parser.getMatch(c.url, c.partialCheck); match { + if match := parser.getMatch(c.url, c.url, &ctxParams, c.partialCheck); match { // Get params from the original path matchRes = true - params = parser.paramsForPos(c.url, paramPos) } } utils.AssertEqual(t, c.match, matchRes, fmt.Sprintf("route: '%s', url: '%s'", r, c.url)) - if matchRes && params != nil { - utils.AssertEqual(t, c.params, params, fmt.Sprintf("route: '%s', url: '%s'", r, c.url)) - } else { - utils.AssertEqual(t, true, nil == params, fmt.Sprintf("route: '%s', url: '%s'", r, c.url)) + if matchRes && len(c.params) > 0 { + utils.AssertEqual(t, c.params[0:len(c.params)-1], ctxParams[0:len(c.params)-1], fmt.Sprintf("route: '%s', url: '%s'", r, c.url)) } }) @@ -429,22 +426,22 @@ func Benchmark_Path_matchParams(t *testing.B) { {url: "/api/v2", params: nil, match: false}, {url: "/api/v1/", params: nil, match: false}, }) -} - -// go test -race -run Test_Reset_StartParamPosList -func Test_Reset_StartParamPosList(t *testing.T) { - atomic.StoreUint32(&startParamPosList, uint32(len(paramsPosDummy))-10) - - getAllocFreeParamsPos(5) - - utils.AssertEqual(t, uint32(5), startParamPosList) -} - -// go test -race -run Test_Reset_startParamList -func Test_Reset_startParamList(t *testing.T) { - atomic.StoreUint32(&startParamList, uint32(len(paramsDummy))-10) - - getAllocFreeParams(5) - - utils.AssertEqual(t, uint32(5), startParamList) + benchCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: true}, + {url: "/api/v1/entity/8728382", params: nil, match: false}, + {url: "/api/v1", params: nil, match: false}, + {url: "/api/v1/", params: nil, match: false}, + }) + benchCase("/api/v1", []testparams{ + {url: "/api/v1", params: []string{}, match: true}, + {url: "/api/v2", params: nil, match: false}, + }) + benchCase("/api/v1/:param/*", []testparams{ + {url: "/api/v1/entity", params: []string{"entity", ""}, match: true}, + {url: "/api/v1/entity/", params: []string{"entity", ""}, match: true}, + {url: "/api/v1/entity/1", params: []string{"entity", "1"}, match: true}, + {url: "/api/v", params: nil, match: false}, + {url: "/api/v2", params: nil, match: false}, + {url: "/api/v1/", params: nil, match: false}, + }) } diff --git a/prefork.go b/prefork.go index 0380c9b6..7516e979 100644 --- a/prefork.go +++ b/prefork.go @@ -11,7 +11,7 @@ import ( "strings" "time" - reuseport "github.com/valyala/fasthttp/reuseport" + "github.com/valyala/fasthttp/reuseport" ) const ( @@ -24,33 +24,28 @@ var ( ) // IsChild determines if the current process is a result of Prefork -func (app *App) IsChild() bool { +func IsChild() bool { return os.Getenv(envPreforkChildKey) == envPreforkChildVal } // prefork manages child processes to make use of the OS REUSEPORT or REUSEADDR feature -func (app *App) prefork(addr string, tlsconfig ...*tls.Config) (err error) { +func (app *App) prefork(addr string, tlsConfig *tls.Config) (err error) { // ๐Ÿ‘ถ child process ๐Ÿ‘ถ - if app.IsChild() { + if IsChild() { // use 1 cpu core per child process runtime.GOMAXPROCS(1) var ln net.Listener - // Set correct network protocol - network := "tcp4" - if isIPv6(addr) { - network = "tcp6" - } // Linux will use SO_REUSEPORT and Windows falls back to SO_REUSEADDR // Only tcp4 or tcp6 is supported when preforking, both are not supported - if ln, err = reuseport.Listen(network, addr); err != nil { - if !app.Settings.DisableStartupMessage { + if ln, err = reuseport.Listen("tcp4", addr); err != nil { + if !app.config.DisableStartupMessage { time.Sleep(100 * time.Millisecond) // avoid colliding with startup message } return fmt.Errorf("prefork: %v", err) } // wrap a tls config around the listener if provided - if len(tlsconfig) > 0 { - ln = tls.NewListener(ln, tlsconfig[0]) + if tlsConfig != nil { + ln = tls.NewListener(ln, tlsConfig) } // kill current child proc when master exits @@ -113,8 +108,8 @@ func (app *App) prefork(addr string, tlsconfig ...*tls.Config) (err error) { } // Print startup message - if !app.Settings.DisableStartupMessage { - app.startupMessage(addr, len(tlsconfig) > 0, ","+strings.Join(pids, ",")) + if !app.config.DisableStartupMessage { + app.startupMessage(addr, tlsConfig != nil, ","+strings.Join(pids, ",")) } // return error if child crashes diff --git a/prefork_test.go b/prefork_test.go index d9553c97..0f162f2b 100644 --- a/prefork_test.go +++ b/prefork_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - utils "github.com/gofiber/utils" + "github.com/gofiber/fiber/v2/utils" ) func Test_App_Prefork_Child_Process(t *testing.T) { @@ -18,9 +18,8 @@ func Test_App_Prefork_Child_Process(t *testing.T) { defer os.Setenv(envPreforkChildKey, "") app := New() - app.init() - err := app.prefork("invalid") + err := app.prefork("invalid", nil) utils.AssertEqual(t, false, err == nil) go func() { @@ -28,10 +27,10 @@ func Test_App_Prefork_Child_Process(t *testing.T) { utils.AssertEqual(t, nil, app.Shutdown()) }() - utils.AssertEqual(t, nil, app.prefork("[::]:")) + utils.AssertEqual(t, nil, app.prefork("[::]:", nil)) // Create tls certificate - cer, err := tls.LoadX509KeyPair("./.github/TEST_DATA/ssl.pem", "./.github/TEST_DATA/ssl.key") + cer, err := tls.LoadX509KeyPair("./.github/testdata/ssl.pem", "./.github/testdata/ssl.key") if err != nil { utils.AssertEqual(t, nil, err) } @@ -50,40 +49,20 @@ func Test_App_Prefork_Master_Process(t *testing.T) { testPreforkMaster = true app := New() - app.init() go func() { time.Sleep(1000 * time.Millisecond) utils.AssertEqual(t, nil, app.Shutdown()) }() - utils.AssertEqual(t, nil, app.prefork(":3000")) + utils.AssertEqual(t, nil, app.prefork(":3000", nil)) dummyChildCmd = "invalid" - err := app.prefork("127.0.0.1:") + err := app.prefork("127.0.0.1:", nil) utils.AssertEqual(t, false, err == nil) } -func Test_App_Prefork_TCP6_Addr(t *testing.T) { - // Reset test var - testPreforkMaster = true - - app := New() - app.Settings.Prefork = true - app.Settings.DisableStartupMessage = true - - app.init() - - go func() { - time.Sleep(1000 * time.Millisecond) - utils.AssertEqual(t, nil, app.Shutdown()) - }() - - err := app.Listen("[::1]:3200") - utils.AssertEqual(t, true, err != nil) -} - func Test_App_Prefork_Child_Process_Never_Show_Startup_Message(t *testing.T) { utils.AssertEqual(t, nil, os.Setenv(envPreforkChildKey, envPreforkChildVal)) defer os.Setenv(envPreforkChildKey, "") diff --git a/router.go b/router.go index bd26076c..aac9008d 100644 --- a/router.go +++ b/router.go @@ -10,8 +10,8 @@ import ( "strings" "time" - utils "github.com/gofiber/utils" - fasthttp "github.com/valyala/fasthttp" + "github.com/gofiber/fiber/v2/utils" + "github.com/valyala/fasthttp" ) // Router defines all router handle interface includes app and group router. @@ -52,108 +52,156 @@ type Route struct { Handlers []Handler `json:"-"` // Ctx handlers } -func (r *Route) match(path, original string) (match bool, values []string) { +func (r *Route) match(path, original string, params *[maxParams]string) (match bool) { // root path check if r.root && path == "/" { - return true, values + return true // '*' wildcard matches any path } else if r.star { - values := getAllocFreeParams(1) if len(original) > 1 { - values[0] = original[1:] + params[0] = original[1:] } - return true, values + return true } // Does this route have parameters if len(r.Params) > 0 { // Match params - if paramPos, match := r.routeParser.getMatch(path, r.use); match { + if match := r.routeParser.getMatch(path, original, params, r.use); match { // Get params from the original path - return match, r.routeParser.paramsForPos(original, paramPos) + return match } } // Is this route a Middleware? if r.use { // Single slash will match or path prefix if r.root || strings.HasPrefix(path, r.path) { - return true, values + return true } // Check for a simple path match } else if len(r.path) == len(path) && r.path == path { - return true, values + return true } // No match - return false, values + return false } -func (app *App) next(ctx *Ctx) bool { +func (app *App) next(c *Ctx) (match bool, err error) { // Get stack length - tree, ok := app.treeStack[ctx.methodINT][ctx.treePath] + tree, ok := app.treeStack[c.methodINT][c.treePath] if !ok { - tree = app.treeStack[ctx.methodINT][""] + tree = app.treeStack[c.methodINT][""] } lenr := len(tree) - 1 + // Loop over the route stack starting from previous index - for ctx.indexRoute < lenr { + for c.indexRoute < lenr { // Increment route index - ctx.indexRoute++ + c.indexRoute++ + // Get *Route - route := tree[ctx.indexRoute] + route := tree[c.indexRoute] + // Check if it matches the request path - match, values := route.match(ctx.path, ctx.pathOriginal) + match = route.match(c.path, c.pathOriginal, &c.values) + // No match, next route if !match { continue } // Pass route reference and param values - ctx.route = route + c.route = route + // Non use handler matched - if !ctx.matched && !route.use { - ctx.matched = true + if !c.matched && !route.use { + c.matched = true } - ctx.values = values // Execute first handler of route - ctx.indexHandler = 0 - route.Handlers[0](ctx) - // Stop scanning the stack - return true + c.indexHandler = 0 + if err = route.Handlers[0](c); err != nil { + if catch := c.app.config.ErrorHandler(c, err); catch != nil { + _ = c.SendStatus(StatusInternalServerError) + } + } + return // Stop scanning the stack } - // If c.Next() does not match, return 404 - ctx.SendStatus(StatusNotFound) - ctx.SendString("Cannot " + ctx.method + " " + ctx.pathOriginal) - // Scan stack for other methods - // Moved from app.handler - // It should be here, - // because middleware may break the route chain - if !ctx.matched { - setMethodNotAllowed(ctx) + // If c.Next() does not match, return 404 + _ = c.SendStatus(StatusNotFound) + _ = c.SendString("Cannot " + c.method + " " + c.pathOriginal) + + // If no match, scan stack again if other methods match the request + // Moved from app.handler because middleware may break the route chain + if !c.matched && methodExist(c) { + if catch := c.app.config.ErrorHandler(c, ErrMethodNotAllowed); catch != nil { + _ = c.SendStatus(StatusInternalServerError) + } } - return false + return } func (app *App) handler(rctx *fasthttp.RequestCtx) { // Acquire Ctx with fasthttp request from pool - ctx := app.AcquireCtx(rctx) + c := app.AcquireCtx(rctx) // handle invalid http method directly - if ctx.methodINT == -1 { - ctx.Status(StatusBadRequest).SendString("Invalid http method") - app.ReleaseCtx(ctx) + if c.methodINT == -1 { + _ = c.Status(StatusBadRequest).SendString("Invalid http method") + app.ReleaseCtx(c) return } + // Find match in stack - match := app.next(ctx) + match, _ := app.next(c) // Generate ETag if enabled - if match && app.Settings.ETag { - setETag(ctx, false) + if match && app.config.ETag { + setETag(c, false) } // Release Ctx - app.ReleaseCtx(ctx) + app.ReleaseCtx(c) } -func (app *App) register(method, pathRaw string, handlers ...Handler) Route { +func (app *App) addPrefixToRoute(prefix string, route *Route) *Route { + prefixedPath := getGroupPath(prefix, route.Path) + prettyPath := prefixedPath + // Case sensitive routing, all to lowercase + if !app.config.CaseSensitive { + prettyPath = utils.ToLower(prettyPath) + } + // Strict routing, remove trailing slashes + if !app.config.StrictRouting && len(prettyPath) > 1 { + prettyPath = utils.TrimRight(prettyPath, '/') + } + + route.Path = prefixedPath + route.path = prettyPath + route.routeParser = parseRoute(prettyPath) + route.root = false + route.star = false + + return route +} + +func (app *App) copyRoute(route *Route) *Route { + return &Route{ + // Router booleans + use: route.use, + star: route.star, + root: route.root, + + // Path data + path: route.path, + routeParser: route.routeParser, + Params: route.Params, + + // Public data + Path: route.path, + Method: route.Method, + Handlers: route.Handlers, + } +} + +func (app *App) register(method, pathRaw string, handlers ...Handler) Router { // Uppercase HTTP methods method = utils.ToUpper(method) // Check if the HTTP method is valid unless it's USE @@ -175,11 +223,11 @@ func (app *App) register(method, pathRaw string, handlers ...Handler) Route { // Create a stripped path in-case sensitive / trailing slashes pathPretty := pathRaw // Case sensitive routing, all to lowercase - if !app.Settings.CaseSensitive { + if !app.config.CaseSensitive { pathPretty = utils.ToLower(pathPretty) } // Strict routing, remove trailing slashes - if !app.Settings.StrictRouting && len(pathPretty) > 1 { + if !app.config.StrictRouting && len(pathPretty) > 1 { pathPretty = utils.TrimRight(pathPretty, '/') } // Is layer a middleware? @@ -198,6 +246,7 @@ func (app *App) register(method, pathRaw string, handlers ...Handler) Route { use: isUse, star: isStar, root: isRoot, + // Path data path: pathPretty, routeParser: parsedPretty, @@ -212,23 +261,25 @@ func (app *App) register(method, pathRaw string, handlers ...Handler) Route { app.mutex.Lock() app.handlerCount += len(handlers) app.mutex.Unlock() + // Middleware route matches all HTTP methods if isUse { // Add route to all HTTP methods stack for _, m := range intMethod { - // create a route copy + // Create a route copy to avoid duplicates during compression r := route app.addRoute(m, &r) } - return route + } else { + // Add route to stack + app.addRoute(method, &route) } - - // Add route to stack - app.addRoute(method, &route) - return route + // Build router tree + app.buildTree() + return app } -func (app *App) registerStatic(prefix, root string, config ...Static) Route { +func (app *App) registerStatic(prefix, root string, config ...Static) Router { // For security we want to restrict to the current work directory. if len(root) == 0 { root = "." @@ -242,7 +293,7 @@ func (app *App) registerStatic(prefix, root string, config ...Static) Route { prefix = "/" + prefix } // in case sensitive routing, all to lowercase - if !app.Settings.CaseSensitive { + if !app.config.CaseSensitive { prefix = utils.ToLower(prefix) } // Strip trailing slashes from the root path @@ -267,11 +318,11 @@ func (app *App) registerStatic(prefix, root string, config ...Static) Route { GenerateIndexPages: false, AcceptByteRange: false, Compress: false, - CompressedFileSuffix: app.Settings.CompressedFileSuffix, + CompressedFileSuffix: app.config.CompressedFileSuffix, CacheDuration: 10 * time.Second, IndexNames: []string{"index.html"}, - PathRewrite: func(ctx *fasthttp.RequestCtx) []byte { - path := ctx.Path() + PathRewrite: func(fctx *fasthttp.RequestCtx) []byte { + path := fctx.Path() if len(path) >= prefixLen { if isStar && getString(path[0:prefixLen]) == prefix { path = append(path[0:0], '/') @@ -284,8 +335,8 @@ func (app *App) registerStatic(prefix, root string, config ...Static) Route { } return path }, - PathNotFound: func(ctx *fasthttp.RequestCtx) { - ctx.Response.SetStatusCode(StatusNotFound) + PathNotFound: func(fctx *fasthttp.RequestCtx) { + fctx.Response.SetStatusCode(StatusNotFound) }, } // Set config if provided @@ -298,20 +349,20 @@ func (app *App) registerStatic(prefix, root string, config ...Static) Route { } } fileHandler := fs.NewRequestHandler() - handler := func(c *Ctx) { + handler := func(c *Ctx) error { // Serve file - fileHandler(c.Fasthttp) + fileHandler(c.fasthttp) // Return request if found and not forbidden - status := c.Fasthttp.Response.StatusCode() + status := c.fasthttp.Response.StatusCode() if status != StatusNotFound && status != StatusForbidden { - return + return nil } // Reset response to default - c.Fasthttp.SetContentType("") // Issue #420 - c.Fasthttp.Response.SetStatusCode(StatusOK) - c.Fasthttp.Response.SetBodyString("") + c.fasthttp.SetContentType("") // Issue #420 + c.fasthttp.Response.SetStatusCode(StatusOK) + c.fasthttp.Response.SetBodyString("") // Next middleware - c.Next() + return c.Next() } // Create route metadata without pointer @@ -332,9 +383,10 @@ func (app *App) registerStatic(prefix, root string, config ...Static) Route { // Add route to stack app.addRoute(MethodGet, &route) // Add HEAD route - headRoute := route - app.addRoute(MethodHead, &headRoute) - return route + app.addRoute(MethodHead, &route) + // Build router tree + app.buildTree() + return app } func (app *App) addRoute(method string, route *Route) { diff --git a/router_test.go b/router_test.go index 426ca2fe..50c23635 100644 --- a/router_test.go +++ b/router_test.go @@ -13,14 +13,14 @@ import ( "net/http/httptest" "testing" - utils "github.com/gofiber/utils" - fasthttp "github.com/valyala/fasthttp" + "github.com/gofiber/fiber/v2/utils" + "github.com/valyala/fasthttp" ) var routesFixture = routeJSON{} func init() { - dat, err := ioutil.ReadFile("./.github/TEST_DATA/testRoutes.json") + dat, err := ioutil.ReadFile("./.github/testdata/testRoutes.json") if err != nil { panic(err) } @@ -32,8 +32,8 @@ func init() { func Test_Route_Match_SameLength(t *testing.T) { app := New() - app.Get("/:param", func(ctx *Ctx) { - ctx.Send(ctx.Params("param")) + app.Get("/:param", func(c *Ctx) error { + return c.SendString(c.Params("param")) }) resp, err := app.Test(httptest.NewRequest(MethodGet, "/:param", nil)) @@ -57,8 +57,8 @@ func Test_Route_Match_SameLength(t *testing.T) { func Test_Route_Match_Star(t *testing.T) { app := New() - app.Get("/*", func(ctx *Ctx) { - ctx.Send(ctx.Params("*")) + app.Get("/*", func(c *Ctx) error { + return c.SendString(c.Params("*")) }) resp, err := app.Test(httptest.NewRequest(MethodGet, "/*", nil)) @@ -84,16 +84,17 @@ func Test_Route_Match_Star(t *testing.T) { path: "/*", routeParser: routeParser{}, } - match, params := route.match("", "") + params := [maxParams]string{} + match := route.match("", "", ¶ms) utils.AssertEqual(t, true, match) - utils.AssertEqual(t, []string{""}, params) + utils.AssertEqual(t, [maxParams]string{}, params) } func Test_Route_Match_Root(t *testing.T) { app := New() - app.Get("/", func(ctx *Ctx) { - ctx.Send("root") + app.Get("/", func(c *Ctx) error { + return c.SendString("root") }) resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil)) @@ -108,11 +109,11 @@ func Test_Route_Match_Root(t *testing.T) { func Test_Route_Match_Parser(t *testing.T) { app := New() - app.Get("/foo/:ParamName", func(ctx *Ctx) { - ctx.Send(ctx.Params("ParamName")) + app.Get("/foo/:ParamName", func(c *Ctx) error { + return c.SendString(c.Params("ParamName")) }) - app.Get("/Foobar/*", func(ctx *Ctx) { - ctx.Send(ctx.Params("*")) + app.Get("/Foobar/*", func(c *Ctx) error { + return c.SendString(c.Params("*")) }) resp, err := app.Test(httptest.NewRequest(MethodGet, "/foo/bar", nil)) utils.AssertEqual(t, nil, err, "app.Test(req)") @@ -135,8 +136,8 @@ func Test_Route_Match_Parser(t *testing.T) { func Test_Route_Match_Middleware(t *testing.T) { app := New() - app.Use("/foo/*", func(ctx *Ctx) { - ctx.Send(ctx.Params("*")) + app.Use("/foo/*", func(c *Ctx) error { + return c.SendString(c.Params("*")) }) resp, err := app.Test(httptest.NewRequest(MethodGet, "/foo/*", nil)) @@ -158,10 +159,10 @@ func Test_Route_Match_Middleware(t *testing.T) { } func Test_Route_Match_UnescapedPath(t *testing.T) { - app := New(&Settings{UnescapePath: true}) + app := New(Config{UnescapePath: true}) - app.Use("/crรฉer", func(ctx *Ctx) { - ctx.Send("test") + app.Use("/crรฉer", func(c *Ctx) error { + return c.SendString("test") }) resp, err := app.Test(httptest.NewRequest(MethodGet, "/cr%C3%A9er", nil)) @@ -177,7 +178,7 @@ func Test_Route_Match_UnescapedPath(t *testing.T) { utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") // check deactivated behavior - app.Settings.UnescapePath = false + app.config.UnescapePath = false resp, err = app.Test(httptest.NewRequest(MethodGet, "/cr%C3%A9er", nil)) utils.AssertEqual(t, nil, err, "app.Test(req)") utils.AssertEqual(t, StatusNotFound, resp.StatusCode, "Status code") @@ -186,8 +187,8 @@ func Test_Route_Match_UnescapedPath(t *testing.T) { func Test_Route_Match_Middleware_HasPrefix(t *testing.T) { app := New() - app.Use("/foo", func(ctx *Ctx) { - ctx.Send("middleware") + app.Use("/foo", func(c *Ctx) error { + return c.SendString("middleware") }) resp, err := app.Test(httptest.NewRequest(MethodGet, "/foo/bar", nil)) @@ -202,8 +203,8 @@ func Test_Route_Match_Middleware_HasPrefix(t *testing.T) { func Test_Route_Match_Middleware_Root(t *testing.T) { app := New() - app.Use("/", func(ctx *Ctx) { - ctx.Send("middleware") + app.Use("/", func(c *Ctx) error { + return c.SendString("middleware") }) resp, err := app.Test(httptest.NewRequest(MethodGet, "/everything", nil)) @@ -237,14 +238,15 @@ func Test_Ensure_Router_Interface_Implementation(t *testing.T) { func Test_Router_Handler_SetETag(t *testing.T) { app := New() - app.Settings.ETag = true + app.config.ETag = true - app.Get("/", func(c *Ctx) { - c.Send("Hello, World!") + app.Get("/", func(c *Ctx) error { + return c.SendString("Hello, World!") }) c := &fasthttp.RequestCtx{} - app.init().handler(c) + + app.handler(c) utils.AssertEqual(t, `"13-1831710635"`, string(c.Response.Header.Peek(HeaderETag))) } @@ -254,7 +256,9 @@ func Test_Router_Handler_SetETag(t *testing.T) { ////////////////////////////////////////////// func registerDummyRoutes(app *App) { - h := func(c *Ctx) {} + h := func(c *Ctx) error { + return nil + } for _, r := range routesFixture.GithubAPI { app.Add(r.Method, r.Path, h) } @@ -263,8 +267,8 @@ func registerDummyRoutes(app *App) { // go test -v -run=^$ -bench=Benchmark_App_MethodNotAllowed -benchmem -count=4 func Benchmark_App_MethodNotAllowed(b *testing.B) { app := New() - h := func(c *Ctx) { - c.Send("Hello World!") + h := func(c *Ctx) error { + return c.SendString("Hello World!") } app.All("/this/is/a/", h) app.Get("/this/is/a/dummy/route/oke", h) @@ -273,6 +277,7 @@ func Benchmark_App_MethodNotAllowed(b *testing.B) { c.Request.Header.SetMethod("DELETE") c.URI().SetPath("/this/is/a/dummy/route/oke") + b.ResetTimer() for n := 0; n < b.N; n++ { app.handler(c) } @@ -284,8 +289,8 @@ func Benchmark_App_MethodNotAllowed(b *testing.B) { // go test -v ./... -run=^$ -bench=Benchmark_Router_NotFound -benchmem -count=4 func Benchmark_Router_NotFound(b *testing.B) { app := New() - app.Use(func(c *Ctx) { - c.Next() + app.Use(func(c *Ctx) error { + return c.Next() }) registerDummyRoutes(app) c := &fasthttp.RequestCtx{} @@ -293,6 +298,7 @@ func Benchmark_Router_NotFound(b *testing.B) { c.Request.Header.SetMethod("DELETE") c.URI().SetPath("/this/route/does/not/exist") + b.ResetTimer() for n := 0; n < b.N; n++ { app.handler(c) } @@ -310,13 +316,15 @@ func Benchmark_Router_Handler(b *testing.B) { c.Request.Header.SetMethod("DELETE") c.URI().SetPath("/user/keys/1337") + b.ResetTimer() + for n := 0; n < b.N; n++ { app.handler(c) } } func Benchmark_Router_Handler_Strict_Case(b *testing.B) { - app := New(&Settings{ + app := New(Config{ StrictRouting: true, CaseSensitive: true, }) @@ -327,6 +335,8 @@ func Benchmark_Router_Handler_Strict_Case(b *testing.B) { c.Request.Header.SetMethod("DELETE") c.URI().SetPath("/user/keys/1337") + b.ResetTimer() + for n := 0; n < b.N; n++ { app.handler(c) } @@ -335,8 +345,8 @@ func Benchmark_Router_Handler_Strict_Case(b *testing.B) { // go test -v ./... -run=^$ -bench=Benchmark_Router_Chain -benchmem -count=4 func Benchmark_Router_Chain(b *testing.B) { app := New() - handler := func(c *Ctx) { - c.Next() + handler := func(c *Ctx) error { + return c.Next() } app.Get("/", handler, handler, handler, handler, handler, handler) @@ -344,7 +354,7 @@ func Benchmark_Router_Chain(b *testing.B) { c.Request.Header.SetMethod("GET") c.URI().SetPath("/") - + b.ResetTimer() for n := 0; n < b.N; n++ { app.handler(c) } @@ -353,8 +363,8 @@ func Benchmark_Router_Chain(b *testing.B) { // go test -v ./... -run=^$ -bench=Benchmark_Router_WithCompression -benchmem -count=4 func Benchmark_Router_WithCompression(b *testing.B) { app := New() - handler := func(c *Ctx) { - c.Next() + handler := func(c *Ctx) error { + return c.Next() } app.Get("/", handler) app.Get("/", handler) @@ -367,7 +377,7 @@ func Benchmark_Router_WithCompression(b *testing.B) { c.Request.Header.SetMethod("GET") c.URI().SetPath("/") - + b.ResetTimer() for n := 0; n < b.N; n++ { app.handler(c) } @@ -383,15 +393,17 @@ func Benchmark_Router_Next(b *testing.B) { request.Request.Header.SetMethod("DELETE") request.URI().SetPath("/user/keys/1337") var res bool + var err error - app.init() c := app.AcquireCtx(request) defer app.ReleaseCtx(c) + b.ResetTimer() for n := 0; n < b.N; n++ { c.indexRoute = -1 - res = app.next(c) + res, err = app.next(c) } + utils.AssertEqual(b, nil, err) utils.AssertEqual(b, true, res) utils.AssertEqual(b, 4, c.indexRoute) } @@ -399,7 +411,7 @@ func Benchmark_Router_Next(b *testing.B) { // go test -v ./... -run=^$ -bench=Benchmark_Route_Match -benchmem -count=4 func Benchmark_Route_Match(b *testing.B) { var match bool - var params []string + var params [maxParams]string parsed := parseRoute("/user/keys/:id") route := &Route{ @@ -413,19 +425,22 @@ func Benchmark_Route_Match(b *testing.B) { Path: "/user/keys/:id", Method: "DELETE", } - route.Handlers = append(route.Handlers, func(c *Ctx) {}) + route.Handlers = append(route.Handlers, func(c *Ctx) error { + return nil + }) + b.ResetTimer() for n := 0; n < b.N; n++ { - match, params = route.match("/user/keys/1337", "/user/keys/1337") + match = route.match("/user/keys/1337", "/user/keys/1337", ¶ms) } utils.AssertEqual(b, true, match) - utils.AssertEqual(b, []string{"1337"}, params) + utils.AssertEqual(b, []string{"1337"}, params[0:len(parsed.params)]) } // go test -v ./... -run=^$ -bench=Benchmark_Route_Match_Star -benchmem -count=4 func Benchmark_Route_Match_Star(b *testing.B) { var match bool - var params []string + var params [maxParams]string parsed := parseRoute("/*") route := &Route{ @@ -439,19 +454,23 @@ func Benchmark_Route_Match_Star(b *testing.B) { Path: "/user/keys/bla", Method: "DELETE", } - route.Handlers = append(route.Handlers, func(c *Ctx) {}) + route.Handlers = append(route.Handlers, func(c *Ctx) error { + return nil + }) + b.ResetTimer() + for n := 0; n < b.N; n++ { - match, params = route.match("/user/keys/bla", "/user/keys/bla") + match = route.match("/user/keys/bla", "/user/keys/bla", ¶ms) } utils.AssertEqual(b, true, match) - utils.AssertEqual(b, []string{"user/keys/bla"}, params) + utils.AssertEqual(b, []string{"user/keys/bla"}, params[0:len(parsed.params)]) } // go test -v ./... -run=^$ -bench=Benchmark_Route_Match_Root -benchmem -count=4 func Benchmark_Route_Match_Root(b *testing.B) { var match bool - var params []string + var params [maxParams]string parsed := parseRoute("/") route := &Route{ @@ -465,19 +484,24 @@ func Benchmark_Route_Match_Root(b *testing.B) { Path: "/", Method: "DELETE", } - route.Handlers = append(route.Handlers, func(c *Ctx) {}) + route.Handlers = append(route.Handlers, func(c *Ctx) error { + return nil + }) + + b.ResetTimer() + for n := 0; n < b.N; n++ { - match, params = route.match("/", "/") + match = route.match("/", "/", ¶ms) } utils.AssertEqual(b, true, match) - utils.AssertEqual(b, []string(nil), params) + utils.AssertEqual(b, []string{}, params[0:len(parsed.params)]) } // go test -v ./... -run=^$ -bench=Benchmark_Router_Handler_CaseSensitive -benchmem -count=4 func Benchmark_Router_Handler_CaseSensitive(b *testing.B) { app := New() - app.Settings.CaseSensitive = true + app.config.CaseSensitive = true registerDummyRoutes(app) c := &fasthttp.RequestCtx{} @@ -485,6 +509,8 @@ func Benchmark_Router_Handler_CaseSensitive(b *testing.B) { c.Request.Header.SetMethod("DELETE") c.URI().SetPath("/user/keys/1337") + b.ResetTimer() + for n := 0; n < b.N; n++ { app.handler(c) } @@ -493,25 +519,29 @@ func Benchmark_Router_Handler_CaseSensitive(b *testing.B) { // go test -v ./... -run=^$ -bench=Benchmark_Router_Handler_Unescape -benchmem -count=4 func Benchmark_Router_Handler_Unescape(b *testing.B) { app := New() - app.Settings.UnescapePath = true + app.config.UnescapePath = true registerDummyRoutes(app) - app.Delete("/crรฉer", func(c *Ctx) {}) + app.Delete("/crรฉer", func(c *Ctx) error { + return nil + }) c := &fasthttp.RequestCtx{} c.Request.Header.SetMethod(MethodDelete) c.URI().SetPath("/cr%C3%A9er") + b.ResetTimer() + for n := 0; n < b.N; n++ { c.URI().SetPath("/cr%C3%A9er") app.handler(c) } } -// go test -v ./... -run=^$ -bench=Benchmark_Router_Handler_StrictRouting -benchmem -count=4 +// go test -run=^$ -bench=Benchmark_Router_Handler_StrictRouting -benchmem -count=4 func Benchmark_Router_Handler_StrictRouting(b *testing.B) { app := New() - app.Settings.CaseSensitive = true + app.config.CaseSensitive = true registerDummyRoutes(app) c := &fasthttp.RequestCtx{} @@ -519,30 +549,33 @@ func Benchmark_Router_Handler_StrictRouting(b *testing.B) { c.Request.Header.SetMethod("DELETE") c.URI().SetPath("/user/keys/1337") + b.ResetTimer() + for n := 0; n < b.N; n++ { app.handler(c) } } -// go test -v ./... -run=^$ -bench=Benchmark_Router_Github_API -benchmem -count=4 +// go test -run=^$ -bench=Benchmark_Router_Github_API -benchmem -count=16 func Benchmark_Router_Github_API(b *testing.B) { app := New() registerDummyRoutes(app) - app.init() c := &fasthttp.RequestCtx{} var match bool + var err error + + b.ResetTimer() for i := range routesFixture.TestRoutes { c.Request.Header.SetMethod(routesFixture.TestRoutes[i].Method) - for n := 0; n < b.N; n++ { c.URI().SetPath(routesFixture.TestRoutes[i].Path) ctx := app.AcquireCtx(c) - match = app.next(ctx) + match, err = app.next(ctx) app.ReleaseCtx(ctx) } - + utils.AssertEqual(b, nil, err) utils.AssertEqual(b, true, match) } diff --git a/utils.go b/utils.go index b8d0c9e1..e07b53f3 100644 --- a/utils.go +++ b/utils.go @@ -6,20 +6,72 @@ package fiber import ( "bytes" + "crypto/tls" "fmt" "hash/crc32" "io" "net" "os" "path/filepath" + "reflect" "strings" "time" + "unsafe" - utils "github.com/gofiber/utils" - bytebufferpool "github.com/valyala/bytebufferpool" - fasthttp "github.com/valyala/fasthttp" + "github.com/gofiber/fiber/v2/utils" + "github.com/gofiber/fiber/v2/utils/bytebufferpool" + "github.com/valyala/fasthttp" ) +/* #nosec */ +// lnMetadata will close the listener and return the addr and tls config +func lnMetadata(ln net.Listener) (addr string, cfg *tls.Config) { + // Get addr + addr = ln.Addr().String() + + // Close listener + if err := ln.Close(); err != nil { + return + } + + // Wait for the listener to be closed + var closed bool + for i := 0; i < 10; i++ { + conn, err := net.DialTimeout("tcp4", addr, 3*time.Second) + if err != nil || conn == nil { + closed = true + break + } + _ = conn.Close() + time.Sleep(100 * time.Millisecond) + } + if !closed { + panic("listener: " + addr + ": Only one usage of each socket address (protocol/network address/port) is normally permitted.") + } + + // Get listener type + pointer := reflect.ValueOf(ln) + + // Is it a tls.listener? + if pointer.String() == "<*tls.listener Value>" { + // Copy value from pointer + if val := reflect.Indirect(pointer); val.Type() != nil { + // Get private field from value + if field := val.FieldByName("config"); field.Type() != nil { + // Copy value from pointer field (unsafe) + if newval := reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())); newval.Type() != nil { + // Get element from pointer + if elem := newval.Elem(); elem.Type() != nil { + // Cast value to *tls.Config + cfg = elem.Interface().(*tls.Config) + } + } + } + } + } + return +} + // readContent opens a named file and read content from it func readContent(rf io.ReaderFrom, name string) (n int64, err error) { // Read file @@ -36,14 +88,37 @@ func readContent(rf io.ReaderFrom, name string) (n int64, err error) { // quoteString escape special characters in a given string func quoteString(raw string) string { bb := bytebufferpool.Get() - quoted := string(fasthttp.AppendQuotedArg(bb.B, getBytes(raw))) + // quoted := string(fasthttp.AppendQuotedArg(bb.B, getBytes(raw))) + quoted := getString(fasthttp.AppendQuotedArg(bb.B, getBytes(raw))) bytebufferpool.Put(bb) return quoted } -// Scan stack if other methods match -func setMethodNotAllowed(ctx *Ctx) { - var matched bool +// removeNewLines will replace `\r` and `\n` with an empty space +func removeNewLines(raw string) string { + start := 0 + if start = strings.IndexByte(raw, '\r'); start == -1 { + if start = strings.IndexByte(raw, '\n'); start == -1 { + return raw + } + } + bb := bytebufferpool.Get() + buf := bb.Bytes() + buf = append(buf, raw...) + for i := start; i < len(buf); i++ { + if buf[i] != '\r' && buf[i] != '\n' { + continue + } + buf[i] = ' ' + } + raw = utils.GetString(buf) + bytebufferpool.Put(bb) + + return raw +} + +// Scan stack if other methods match the request +func methodExist(ctx *Ctx) (exist bool) { for i := 0; i < len(intMethod); i++ { // Skip original method if ctx.methodINT == i { @@ -68,11 +143,11 @@ func setMethodNotAllowed(ctx *Ctx) { continue } // Check if it matches the request path - match, _ := route.match(ctx.path, ctx.pathOriginal) + match := route.match(ctx.path, ctx.pathOriginal, &ctx.values) // No match, next route if match { // We matched - matched = true + exist = true // Add method to Allow header ctx.Append(HeaderAllow, intMethod[i]) // Break stack loop @@ -80,10 +155,23 @@ func setMethodNotAllowed(ctx *Ctx) { } } } - // Update response status - if matched { - ctx.Status(StatusMethodNotAllowed) + return +} + +// uniqueRouteStack drop all not unique routes from the slice +func uniqueRouteStack(stack []*Route) []*Route { + var unique []*Route + m := make(map[*Route]int) + for _, v := range stack { + if _, ok := m[v]; !ok { + // Unique key found. Record position and collect + // in result. + m[v] = len(unique) + unique = append(unique, v) + } } + + return unique } // defaultString returns the value or a default value if it is set @@ -94,19 +182,21 @@ func defaultString(value string, defaultValue []string) string { return value } +const normalizedHeaderETag = "Etag" + // Generate and set ETag header to response -func setETag(ctx *Ctx, weak bool) { +func setETag(c *Ctx, weak bool) { // Don't generate ETags for invalid responses - if ctx.Fasthttp.Response.StatusCode() != StatusOK { + if c.fasthttp.Response.StatusCode() != StatusOK { return } - body := ctx.Fasthttp.Response.Body() + body := c.fasthttp.Response.Body() // Skips ETag if no response body is present if len(body) <= 0 { return } // Get ETag header from request - clientEtag := ctx.Get(HeaderIfNoneMatch) + clientEtag := c.Get(HeaderIfNoneMatch) // Generate ETag for response crc32q := crc32.MakeTable(0xD5828281) @@ -122,22 +212,22 @@ func setETag(ctx *Ctx, weak bool) { // Check if server's ETag is weak if clientEtag[2:] == etag || clientEtag[2:] == etag[2:] { // W/1 == 1 || W/1 == W/1 - ctx.SendStatus(StatusNotModified) - ctx.Fasthttp.ResetBody() + _ = c.SendStatus(StatusNotModified) + c.fasthttp.ResetBody() return } // W/1 != W/2 || W/1 != 2 - ctx.Set(HeaderETag, etag) + c.setCanonical(normalizedHeaderETag, etag) return } if strings.Contains(clientEtag, etag) { // 1 == 1 - ctx.SendStatus(StatusNotModified) - ctx.Fasthttp.ResetBody() + _ = c.SendStatus(StatusNotModified) + c.fasthttp.ResetBody() return } // 1 != 2 - ctx.Set(HeaderETag, etag) + c.setCanonical(normalizedHeaderETag, etag) } func getGroupPath(prefix, path string) string { @@ -294,22 +384,6 @@ var getBytesImmutable = func(s string) (b []byte) { return []byte(s) } -// uniqueRouteStack drop all not unique routes from the slice -func uniqueRouteStack(stack []*Route) []*Route { - var unique []*Route - m := make(map[*Route]int) - for _, v := range stack { - if _, ok := m[v]; !ok { - // Unique key found. Record position and collect - // in result. - m[v] = len(unique) - unique = append(unique, v) - } - } - - return unique -} - // HTTP methods and their unique INTs func methodInt(s string) int { switch s { @@ -392,7 +466,7 @@ const ( StatusOK = 200 // RFC 7231, 6.3.1 StatusCreated = 201 // RFC 7231, 6.3.2 StatusAccepted = 202 // RFC 7231, 6.3.3 - StatusNonAuthoritativeInfo = 203 // RFC 7231, 6.3.4 + StatusNonAuthoritativeInformation = 203 // RFC 7231, 6.3.4 StatusNoContent = 204 // RFC 7231, 6.3.5 StatusResetContent = 205 // RFC 7231, 6.3.6 StatusPartialContent = 206 // RFC 7233, 4.1 @@ -451,28 +525,6 @@ const ( // Errors var ( - ErrContinue = NewError(StatusContinue) // RFC 7231, 6.2.1 - ErrSwitchingProtocols = NewError(StatusSwitchingProtocols) // RFC 7231, 6.2.2 - ErrProcessing = NewError(StatusProcessing) // RFC 2518, 10.1 - ErrEarlyHints = NewError(StatusEarlyHints) // RFC 8297 - ErrOK = NewError(StatusOK) // RFC 7231, 6.3.1 - ErrCreated = NewError(StatusCreated) // RFC 7231, 6.3.2 - ErrAccepted = NewError(StatusAccepted) // RFC 7231, 6.3.3 - ErrNonAuthoritativeInfo = NewError(StatusNonAuthoritativeInfo) // RFC 7231, 6.3.4 - ErrNoContent = NewError(StatusNoContent) // RFC 7231, 6.3.5 - ErrResetContent = NewError(StatusResetContent) // RFC 7231, 6.3.6 - ErrPartialContent = NewError(StatusPartialContent) // RFC 7233, 4.1 - ErrMultiStatus = NewError(StatusMultiStatus) // RFC 4918, 11.1 - ErrAlreadyReported = NewError(StatusAlreadyReported) // RFC 5842, 7.1 - ErrIMUsed = NewError(StatusIMUsed) // RFC 3229, 10.4.1 - ErrMultipleChoices = NewError(StatusMultipleChoices) // RFC 7231, 6.4.1 - ErrMovedPermanently = NewError(StatusMovedPermanently) // RFC 7231, 6.4.2 - ErrFound = NewError(StatusFound) // RFC 7231, 6.4.3 - ErrSeeOther = NewError(StatusSeeOther) // RFC 7231, 6.4.4 - ErrNotModified = NewError(StatusNotModified) // RFC 7232, 4.1 - ErrUseProxy = NewError(StatusUseProxy) // RFC 7231, 6.4.5 - ErrTemporaryRedirect = NewError(StatusTemporaryRedirect) // RFC 7231, 6.4.7 - ErrPermanentRedirect = NewError(StatusPermanentRedirect) // RFC 7538, 3 ErrBadRequest = NewError(StatusBadRequest) // RFC 7231, 6.5.1 ErrUnauthorized = NewError(StatusUnauthorized) // RFC 7235, 3.1 ErrPaymentRequired = NewError(StatusPaymentRequired) // RFC 7231, 6.5.2 diff --git a/utils/README.md b/utils/README.md new file mode 100644 index 00000000..ecb8aab1 --- /dev/null +++ b/utils/README.md @@ -0,0 +1,63 @@ +# Utils + +[![Release](https://img.shields.io/github/release/gofiber/fiber.svg)](https://github.com/gofiber/fiber/releases) +[![Discord](https://img.shields.io/discord/704680098577514527?label=Discord&logo=discord&logoColor=white&color=7289DA)](https://gofiber.io/discord) +[![Test](https://github.com/gofiber/utils/workflows/Test/badge.svg)](https://github.com/gofiber/utils/actions?query=workflow%3ATest) +[![Security](https://github.com/gofiber/utils/workflows/Security/badge.svg)](https://github.com/gofiber/utils/actions?query=workflow%3ASecurity) +[![Linter](https://github.com/gofiber/utils/workflows/Linter/badge.svg)](https://github.com/gofiber/utils/actions?query=workflow%3ALinter) + +A collection of common functions but with better performance, less allocations and no dependencies created for [Fiber](https://github.com/gofiber/fiber). + +```go +// go test -v -benchmem -run=^$ -bench=Benchmark_ -count=2 + +Benchmark_GetMIME/fiber 14287550 84.2 ns/op 0 B/op 0 allocs/op +Benchmark_GetMIME/fiber 14819698 78.3 ns/op 0 B/op 0 allocs/op +Benchmark_GetMIME/default 6459128 184 ns/op 0 B/op 0 allocs/op +Benchmark_GetMIME/default 6385042 184 ns/op 0 B/op 0 allocs/op + +Benchmark_UUID/fiber 17652744 59.1 ns/op 48 B/op 1 allocs/op +Benchmark_UUID/fiber 19361145 58.5 ns/op 48 B/op 1 allocs/op +Benchmark_UUID/default 4271024 281 ns/op 64 B/op 2 allocs/op +Benchmark_UUID/default 4435306 278 ns/op 64 B/op 2 allocs/op + +Benchmark_ToLower/fiber 22987184 48.2 ns/op 48 B/op 1 allocs/op +Benchmark_ToLower/fiber 24491794 49.6 ns/op 48 B/op 1 allocs/op +Benchmark_ToLower/default 9232608 123 ns/op 48 B/op 1 allocs/op +Benchmark_ToLower/default 9454870 123 ns/op 48 B/op 1 allocs/op + +Benchmark_ToLowerBytes/fiber 44463876 26.1 ns/op 0 B/op 0 allocs/op +Benchmark_ToLowerBytes/fiber 39997200 26.1 ns/op 0 B/op 0 allocs/op +Benchmark_ToLowerBytes/default 14879088 77.6 ns/op 48 B/op 1 allocs/op +Benchmark_ToLowerBytes/default 14631433 79.2 ns/op 48 B/op 1 allocs/op + +Benchmark_ToUpper/fiber 22648730 49.4 ns/op 48 B/op 1 allocs/op +Benchmark_ToUpper/fiber 23084425 48.6 ns/op 48 B/op 1 allocs/op +Benchmark_ToUpper/default 9520122 124 ns/op 48 B/op 1 allocs/op +Benchmark_ToUpper/default 9375014 133 ns/op 48 B/op 1 allocs/op + +Benchmark_ToUpperBytes/fiber 44439176 25.6 ns/op 0 B/op 0 allocs/op +Benchmark_ToUpperBytes/fiber 44458934 25.5 ns/op 0 B/op 0 allocs/op +Benchmark_ToUpperBytes/default 15347073 74.1 ns/op 48 B/op 1 allocs/op +Benchmark_ToUpperBytes/default 15511370 74.2 ns/op 48 B/op 1 allocs/op + +Benchmark_EqualFolds/fiber 34297864 33.8 ns/op 0 B/op 0 allocs/op +Benchmark_EqualFolds/fiber 34285322 34.0 ns/op 0 B/op 0 allocs/op +Benchmark_EqualFolds/default 12756945 91.8 ns/op 0 B/op 0 allocs/op +Benchmark_EqualFolds/default 13015282 91.1 ns/op 0 B/op 0 allocs/op + +Benchmark_Trim/fiber 207314002 5.85 ns/op 0 B/op 0 allocs/op +Benchmark_Trim/fiber 207386125 5.78 ns/op 0 B/op 0 allocs/op +Benchmark_Trim/default 16506302 68.5 ns/op 32 B/op 1 allocs/op +Benchmark_Trim/default 16669119 68.9 ns/op 32 B/op 1 allocs/op + +Benchmark_TrimLeft/fiber 343254828 3.47 ns/op 0 B/op 0 allocs/op +Benchmark_TrimLeft/fiber 344407171 3.45 ns/op 0 B/op 0 allocs/op +Benchmark_TrimLeft/default 24999790 46.4 ns/op 32 B/op 1 allocs/op +Benchmark_TrimLeft/default 25001926 45.3 ns/op 32 B/op 1 allocs/op + +Benchmark_TrimRight/fiber 374543056 3.15 ns/op 0 B/op 0 allocs/op +Benchmark_TrimRight/fiber 336067616 3.15 ns/op 0 B/op 0 allocs/op +Benchmark_TrimRight/default 20868186 52.8 ns/op 32 B/op 1 allocs/op +Benchmark_TrimRight/default 21434695 55.1 ns/op 32 B/op 1 allocs/op +``` \ No newline at end of file diff --git a/utils/bytebufferpool/LICENSE b/utils/bytebufferpool/LICENSE new file mode 100644 index 00000000..f7c935c2 --- /dev/null +++ b/utils/bytebufferpool/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2016 Aliaksandr Valialkin, VertaMedia + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/utils/bytebufferpool/bytebuffer.go b/utils/bytebufferpool/bytebuffer.go new file mode 100644 index 00000000..07a055a2 --- /dev/null +++ b/utils/bytebufferpool/bytebuffer.go @@ -0,0 +1,111 @@ +package bytebufferpool + +import "io" + +// ByteBuffer provides byte buffer, which can be used for minimizing +// memory allocations. +// +// ByteBuffer may be used with functions appending data to the given []byte +// slice. See example code for details. +// +// Use Get for obtaining an empty byte buffer. +type ByteBuffer struct { + + // B is a byte buffer to use in append-like workloads. + // See example code for details. + B []byte +} + +// Len returns the size of the byte buffer. +func (b *ByteBuffer) Len() int { + return len(b.B) +} + +// ReadFrom implements io.ReaderFrom. +// +// The function appends all the data read from r to b. +func (b *ByteBuffer) ReadFrom(r io.Reader) (int64, error) { + p := b.B + nStart := int64(len(p)) + nMax := int64(cap(p)) + n := nStart + if nMax == 0 { + nMax = 64 + p = make([]byte, nMax) + } else { + p = p[:nMax] + } + for { + if n == nMax { + nMax *= 2 + bNew := make([]byte, nMax) + copy(bNew, p) + p = bNew + } + nn, err := r.Read(p[n:]) + n += int64(nn) + if err != nil { + b.B = p[:n] + n -= nStart + if err == io.EOF { + return n, nil + } + return n, err + } + } +} + +// WriteTo implements io.WriterTo. +func (b *ByteBuffer) WriteTo(w io.Writer) (int64, error) { + n, err := w.Write(b.B) + return int64(n), err +} + +// Bytes returns b.B, i.e. all the bytes accumulated in the buffer. +// +// The purpose of this function is bytes.Buffer compatibility. +func (b *ByteBuffer) Bytes() []byte { + return b.B +} + +// Write implements io.Writer - it appends p to ByteBuffer.B +func (b *ByteBuffer) Write(p []byte) (int, error) { + b.B = append(b.B, p...) + return len(p), nil +} + +// WriteByte appends the byte c to the buffer. +// +// The purpose of this function is bytes.Buffer compatibility. +// +// The function always returns nil. +func (b *ByteBuffer) WriteByte(c byte) error { + b.B = append(b.B, c) + return nil +} + +// WriteString appends s to ByteBuffer.B. +func (b *ByteBuffer) WriteString(s string) (int, error) { + b.B = append(b.B, s...) + return len(s), nil +} + +// Set sets ByteBuffer.B to p. +func (b *ByteBuffer) Set(p []byte) { + b.B = append(b.B[:0], p...) +} + +// SetString sets ByteBuffer.B to s. +func (b *ByteBuffer) SetString(s string) { + b.B = append(b.B[:0], s...) +} + +// String returns string representation of ByteBuffer.B. +func (b *ByteBuffer) String() string { + return string(b.B) +} + +// Reset makes ByteBuffer.B empty. +func (b *ByteBuffer) Reset() { + b.B = b.B[:0] +} diff --git a/utils/bytebufferpool/bytebuffer_test.go b/utils/bytebufferpool/bytebuffer_test.go new file mode 100644 index 00000000..7bb658f8 --- /dev/null +++ b/utils/bytebufferpool/bytebuffer_test.go @@ -0,0 +1,138 @@ +package bytebufferpool + +import ( + "bytes" + "fmt" + "io" + "testing" + "time" +) + +func TestByteBufferReadFrom(t *testing.T) { + prefix := "foobar" + expectedS := "asadfsdafsadfasdfisdsdfa" + prefixLen := int64(len(prefix)) + expectedN := int64(len(expectedS)) + + var bb ByteBuffer + bb.WriteString(prefix) + + rf := (io.ReaderFrom)(&bb) + for i := 0; i < 20; i++ { + r := bytes.NewBufferString(expectedS) + n, err := rf.ReadFrom(r) + if n != expectedN { + t.Fatalf("unexpected n=%d. Expecting %d. iteration %d", n, expectedN, i) + } + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + bbLen := int64(bb.Len()) + expectedLen := prefixLen + int64(i+1)*expectedN + if bbLen != expectedLen { + t.Fatalf("unexpected byteBuffer length: %d. Expecting %d", bbLen, expectedLen) + } + for j := 0; j < i; j++ { + start := prefixLen + int64(j)*expectedN + b := bb.B[start : start+expectedN] + if string(b) != expectedS { + t.Fatalf("unexpected byteBuffer contents: %q. Expecting %q", b, expectedS) + } + } + } +} + +func TestByteBufferWriteTo(t *testing.T) { + expectedS := "foobarbaz" + var bb ByteBuffer + bb.WriteString(expectedS[:3]) + bb.WriteString(expectedS[3:]) + + wt := (io.WriterTo)(&bb) + var w bytes.Buffer + for i := 0; i < 10; i++ { + n, err := wt.WriteTo(&w) + if n != int64(len(expectedS)) { + t.Fatalf("unexpected n returned from WriteTo: %d. Expecting %d", n, len(expectedS)) + } + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + s := string(w.Bytes()) + if s != expectedS { + t.Fatalf("unexpected string written %q. Expecting %q", s, expectedS) + } + w.Reset() + } +} + +func TestByteBufferGetPutSerial(t *testing.T) { + testByteBufferGetPut(t) +} + +func TestByteBufferGetPutConcurrent(t *testing.T) { + concurrency := 10 + ch := make(chan struct{}, concurrency) + for i := 0; i < concurrency; i++ { + go func() { + testByteBufferGetPut(t) + ch <- struct{}{} + }() + } + + for i := 0; i < concurrency; i++ { + select { + case <-ch: + case <-time.After(time.Second): + t.Fatalf("timeout!") + } + } +} + +func testByteBufferGetPut(t *testing.T) { + for i := 0; i < 10; i++ { + expectedS := fmt.Sprintf("num %d", i) + b := Get() + b.B = append(b.B, "num "...) + b.B = append(b.B, fmt.Sprintf("%d", i)...) + if string(b.B) != expectedS { + t.Fatalf("unexpected result: %q. Expecting %q", b.B, expectedS) + } + Put(b) + } +} + +func testByteBufferGetString(t *testing.T) { + for i := 0; i < 10; i++ { + expectedS := fmt.Sprintf("num %d", i) + b := Get() + b.SetString(expectedS) + if b.String() != expectedS { + t.Fatalf("unexpected result: %q. Expecting %q", b.B, expectedS) + } + Put(b) + } +} + +func TestByteBufferGetStringSerial(t *testing.T) { + testByteBufferGetString(t) +} + +func TestByteBufferGetStringConcurrent(t *testing.T) { + concurrency := 10 + ch := make(chan struct{}, concurrency) + for i := 0; i < concurrency; i++ { + go func() { + testByteBufferGetString(t) + ch <- struct{}{} + }() + } + + for i := 0; i < concurrency; i++ { + select { + case <-ch: + case <-time.After(time.Second): + t.Fatalf("timeout!") + } + } +} diff --git a/utils/bytebufferpool/bytebuffer_timing_test.go b/utils/bytebufferpool/bytebuffer_timing_test.go new file mode 100644 index 00000000..29f92de4 --- /dev/null +++ b/utils/bytebufferpool/bytebuffer_timing_test.go @@ -0,0 +1,32 @@ +package bytebufferpool + +import ( + "bytes" + "testing" +) + +func BenchmarkByteBufferWrite(b *testing.B) { + s := []byte("foobarbaz") + b.RunParallel(func(pb *testing.PB) { + var buf ByteBuffer + for pb.Next() { + for i := 0; i < 100; i++ { + buf.Write(s) + } + buf.Reset() + } + }) +} + +func BenchmarkBytesBufferWrite(b *testing.B) { + s := []byte("foobarbaz") + b.RunParallel(func(pb *testing.PB) { + var buf bytes.Buffer + for pb.Next() { + for i := 0; i < 100; i++ { + buf.Write(s) + } + buf.Reset() + } + }) +} diff --git a/utils/bytebufferpool/pool.go b/utils/bytebufferpool/pool.go new file mode 100644 index 00000000..8bb4134d --- /dev/null +++ b/utils/bytebufferpool/pool.go @@ -0,0 +1,151 @@ +package bytebufferpool + +import ( + "sort" + "sync" + "sync/atomic" +) + +const ( + minBitSize = 6 // 2**6=64 is a CPU cache line size + steps = 20 + + minSize = 1 << minBitSize + maxSize = 1 << (minBitSize + steps - 1) + + calibrateCallsThreshold = 42000 + maxPercentile = 0.95 +) + +// Pool represents byte buffer pool. +// +// Distinct pools may be used for distinct types of byte buffers. +// Properly determined byte buffer types with their own pools may help reducing +// memory waste. +type Pool struct { + calls [steps]uint64 + calibrating uint64 + + defaultSize uint64 + maxSize uint64 + + pool sync.Pool +} + +var defaultPool Pool + +// Get returns an empty byte buffer from the pool. +// +// Got byte buffer may be returned to the pool via Put call. +// This reduces the number of memory allocations required for byte buffer +// management. +func Get() *ByteBuffer { return defaultPool.Get() } + +// Get returns new byte buffer with zero length. +// +// The byte buffer may be returned to the pool via Put after the use +// in order to minimize GC overhead. +func (p *Pool) Get() *ByteBuffer { + v := p.pool.Get() + if v != nil { + return v.(*ByteBuffer) + } + return &ByteBuffer{ + B: make([]byte, 0, atomic.LoadUint64(&p.defaultSize)), + } +} + +// Put returns byte buffer to the pool. +// +// ByteBuffer.B mustn't be touched after returning it to the pool. +// Otherwise data races will occur. +func Put(b *ByteBuffer) { defaultPool.Put(b) } + +// Put releases byte buffer obtained via Get to the pool. +// +// The buffer mustn't be accessed after returning to the pool. +func (p *Pool) Put(b *ByteBuffer) { + idx := index(len(b.B)) + + if atomic.AddUint64(&p.calls[idx], 1) > calibrateCallsThreshold { + p.calibrate() + } + + maxSize := int(atomic.LoadUint64(&p.maxSize)) + if maxSize == 0 || cap(b.B) <= maxSize { + b.Reset() + p.pool.Put(b) + } +} + +func (p *Pool) calibrate() { + if !atomic.CompareAndSwapUint64(&p.calibrating, 0, 1) { + return + } + + a := make(callSizes, 0, steps) + var callsSum uint64 + for i := uint64(0); i < steps; i++ { + calls := atomic.SwapUint64(&p.calls[i], 0) + callsSum += calls + a = append(a, callSize{ + calls: calls, + size: minSize << i, + }) + } + sort.Sort(a) + + defaultSize := a[0].size + maxSize := defaultSize + + maxSum := uint64(float64(callsSum) * maxPercentile) + callsSum = 0 + for i := 0; i < steps; i++ { + if callsSum > maxSum { + break + } + callsSum += a[i].calls + size := a[i].size + if size > maxSize { + maxSize = size + } + } + + atomic.StoreUint64(&p.defaultSize, defaultSize) + atomic.StoreUint64(&p.maxSize, maxSize) + + atomic.StoreUint64(&p.calibrating, 0) +} + +type callSize struct { + calls uint64 + size uint64 +} + +type callSizes []callSize + +func (ci callSizes) Len() int { + return len(ci) +} + +func (ci callSizes) Less(i, j int) bool { + return ci[i].calls > ci[j].calls +} + +func (ci callSizes) Swap(i, j int) { + ci[i], ci[j] = ci[j], ci[i] +} + +func index(n int) int { + n-- + n >>= minBitSize + idx := 0 + for n > 0 { + n >>= 1 + idx++ + } + if idx >= steps { + idx = steps - 1 + } + return idx +} diff --git a/utils/bytebufferpool/pool_test.go b/utils/bytebufferpool/pool_test.go new file mode 100644 index 00000000..6d3bcb86 --- /dev/null +++ b/utils/bytebufferpool/pool_test.go @@ -0,0 +1,94 @@ +package bytebufferpool + +import ( + "math/rand" + "testing" + "time" +) + +func TestIndex(t *testing.T) { + testIndex(t, 0, 0) + testIndex(t, 1, 0) + + testIndex(t, minSize-1, 0) + testIndex(t, minSize, 0) + testIndex(t, minSize+1, 1) + + testIndex(t, 2*minSize-1, 1) + testIndex(t, 2*minSize, 1) + testIndex(t, 2*minSize+1, 2) + + testIndex(t, maxSize-1, steps-1) + testIndex(t, maxSize, steps-1) + testIndex(t, maxSize+1, steps-1) +} + +func testIndex(t *testing.T, n, expectedIdx int) { + idx := index(n) + if idx != expectedIdx { + t.Fatalf("unexpected idx for n=%d: %d. Expecting %d", n, idx, expectedIdx) + } +} + +func TestPoolCalibrate(t *testing.T) { + for i := 0; i < steps*calibrateCallsThreshold; i++ { + n := 1004 + if i%15 == 0 { + n = rand.Intn(15234) + } + testGetPut(t, n) + } +} + +func TestPoolVariousSizesSerial(t *testing.T) { + testPoolVariousSizes(t) +} + +func TestPoolVariousSizesConcurrent(t *testing.T) { + concurrency := 5 + ch := make(chan struct{}) + for i := 0; i < concurrency; i++ { + go func() { + testPoolVariousSizes(t) + ch <- struct{}{} + }() + } + for i := 0; i < concurrency; i++ { + select { + case <-ch: + case <-time.After(3 * time.Second): + t.Fatalf("timeout") + } + } +} + +func testPoolVariousSizes(t *testing.T) { + for i := 0; i < steps+1; i++ { + n := (1 << uint32(i)) + + testGetPut(t, n) + testGetPut(t, n+1) + testGetPut(t, n-1) + + for j := 0; j < 10; j++ { + testGetPut(t, j+n) + } + } +} + +func testGetPut(t *testing.T, n int) { + bb := Get() + if len(bb.B) > 0 { + t.Fatalf("non-empty byte buffer returned from acquire") + } + bb.B = allocNBytes(bb.B, n) + Put(bb) +} + +func allocNBytes(dst []byte, n int) []byte { + diff := n - cap(dst) + if diff <= 0 { + return dst[:n] + } + return append(dst, make([]byte, diff)...) +} diff --git a/utils/colorable/LICENSE b/utils/colorable/LICENSE new file mode 100644 index 00000000..91b5cef3 --- /dev/null +++ b/utils/colorable/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Yasuhiro Matsumoto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/utils/colorable/colorable_appengine.go b/utils/colorable/colorable_appengine.go new file mode 100644 index 00000000..ff28d7f0 --- /dev/null +++ b/utils/colorable/colorable_appengine.go @@ -0,0 +1,37 @@ +// +build appengine + +package colorable + +import ( + "io" + "os" + + _ "github.com/gofiber/fiber/v2/utils/isatty" +) + +// NewColorable returns new instance of Writer which handles escape sequence. +func NewColorable(file *os.File) io.Writer { + if file == nil { + panic("nil passed instead of *os.File to NewColorable()") + } + + return file +} + +// NewColorableStdout returns new instance of Writer which handles escape sequence for stdout. +func NewColorableStdout() io.Writer { + return os.Stdout +} + +// NewColorableStderr returns new instance of Writer which handles escape sequence for stderr. +func NewColorableStderr() io.Writer { + return os.Stderr +} + +// EnableColorsStdout enable colors if possible. +func EnableColorsStdout(enabled *bool) func() { + if enabled != nil { + *enabled = true + } + return func() {} +} diff --git a/utils/colorable/colorable_others.go b/utils/colorable/colorable_others.go new file mode 100644 index 00000000..8d1be881 --- /dev/null +++ b/utils/colorable/colorable_others.go @@ -0,0 +1,38 @@ +// +build !windows +// +build !appengine + +package colorable + +import ( + "io" + "os" + + _ "github.com/gofiber/fiber/v2/utils/isatty" +) + +// NewColorable returns new instance of Writer which handles escape sequence. +func NewColorable(file *os.File) io.Writer { + if file == nil { + panic("nil passed instead of *os.File to NewColorable()") + } + + return file +} + +// NewColorableStdout returns new instance of Writer which handles escape sequence for stdout. +func NewColorableStdout() io.Writer { + return os.Stdout +} + +// NewColorableStderr returns new instance of Writer which handles escape sequence for stderr. +func NewColorableStderr() io.Writer { + return os.Stderr +} + +// EnableColorsStdout enable colors if possible. +func EnableColorsStdout(enabled *bool) func() { + if enabled != nil { + *enabled = true + } + return func() {} +} diff --git a/utils/colorable/colorable_test.go b/utils/colorable/colorable_test.go new file mode 100644 index 00000000..b848a4f5 --- /dev/null +++ b/utils/colorable/colorable_test.go @@ -0,0 +1,98 @@ +package colorable + +import ( + "bytes" + "os" + "runtime" + "testing" +) + +// checkEncoding checks that colorable is output encoding agnostic as long as +// the encoding is a superset of ASCII. This implies that one byte not part of +// an ANSI sequence must give exactly one byte in output +func checkEncoding(t *testing.T, data []byte) { + // Send non-UTF8 data to colorable + b := bytes.NewBuffer(make([]byte, 0, 10)) + if b.Len() != 0 { + t.FailNow() + } + // TODO move colorable wrapping outside the test + NewNonColorable(b).Write(data) + if b.Len() != len(data) { + t.Fatalf("%d bytes expected, got %d", len(data), b.Len()) + } +} + +func TestEncoding(t *testing.T) { + checkEncoding(t, []byte{}) // Empty + checkEncoding(t, []byte(`abc`)) // "abc" + checkEncoding(t, []byte(`รฉ`)) // "รฉ" in UTF-8 + checkEncoding(t, []byte{233}) // 'รฉ' in Latin-1 +} + +func TestNonColorable(t *testing.T) { + var buf bytes.Buffer + want := "hello" + NewNonColorable(&buf).Write([]byte("\x1b[0m" + want + "\x1b[2J")) + got := buf.String() + if got != "hello" { + t.Fatalf("want %q but %q", want, got) + } + + buf.Reset() + NewNonColorable(&buf).Write([]byte("\x1b[")) + got = buf.String() + if got != "" { + t.Fatalf("want %q but %q", "", got) + } +} + +func TestNonColorableNil(t *testing.T) { + paniced := false + func() { + defer func() { + recover() + paniced = true + }() + NewNonColorable(nil) + NewColorable(nil) + }() + + if !paniced { + t.Fatalf("should panic") + } +} + +func TestNonColorableESC(t *testing.T) { + var b bytes.Buffer + NewNonColorable(&b).Write([]byte{0x1b}) + if b.Len() > 0 { + t.Fatalf("0 bytes expected, got %d", b.Len()) + } +} + +func TestNonColorableBadESC(t *testing.T) { + var b bytes.Buffer + NewNonColorable(&b).Write([]byte{0x1b, 0x1b}) + if b.Len() > 0 { + t.Fatalf("0 bytes expected, got %d", b.Len()) + } +} + +func TestColorable(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skipf("skip this test on windows") + } + _, ok := NewColorableStdout().(*os.File) + if !ok { + t.Fatalf("should os.Stdout on UNIX") + } + _, ok = NewColorableStderr().(*os.File) + if !ok { + t.Fatalf("should os.Stdout on UNIX") + } + _, ok = NewColorable(os.Stdout).(*os.File) + if !ok { + t.Fatalf("should os.Stdout on UNIX") + } +} diff --git a/utils/colorable/colorable_windows.go b/utils/colorable/colorable_windows.go new file mode 100644 index 00000000..170a5b92 --- /dev/null +++ b/utils/colorable/colorable_windows.go @@ -0,0 +1,1043 @@ +// +build windows +// +build !appengine + +package colorable + +import ( + "bytes" + "io" + "math" + "os" + "strconv" + "strings" + "sync" + "syscall" + "unsafe" + + "github.com/gofiber/fiber/v2/utils/isatty" +) + +const ( + foregroundBlue = 0x1 + foregroundGreen = 0x2 + foregroundRed = 0x4 + foregroundIntensity = 0x8 + foregroundMask = (foregroundRed | foregroundBlue | foregroundGreen | foregroundIntensity) + backgroundBlue = 0x10 + backgroundGreen = 0x20 + backgroundRed = 0x40 + backgroundIntensity = 0x80 + backgroundMask = (backgroundRed | backgroundBlue | backgroundGreen | backgroundIntensity) + commonLvbUnderscore = 0x8000 + + cENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x4 +) + +const ( + genericRead = 0x80000000 + genericWrite = 0x40000000 +) + +const ( + consoleTextmodeBuffer = 0x1 +) + +type wchar uint16 +type short int16 +type dword uint32 +type word uint16 + +type coord struct { + x short + y short +} + +type smallRect struct { + left short + top short + right short + bottom short +} + +type consoleScreenBufferInfo struct { + size coord + cursorPosition coord + attributes word + window smallRect + maximumWindowSize coord +} + +type consoleCursorInfo struct { + size dword + visible int32 +} + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") + procSetConsoleTextAttribute = kernel32.NewProc("SetConsoleTextAttribute") + procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition") + procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW") + procFillConsoleOutputAttribute = kernel32.NewProc("FillConsoleOutputAttribute") + procGetConsoleCursorInfo = kernel32.NewProc("GetConsoleCursorInfo") + procSetConsoleCursorInfo = kernel32.NewProc("SetConsoleCursorInfo") + procSetConsoleTitle = kernel32.NewProc("SetConsoleTitleW") + procGetConsoleMode = kernel32.NewProc("GetConsoleMode") + procSetConsoleMode = kernel32.NewProc("SetConsoleMode") + procCreateConsoleScreenBuffer = kernel32.NewProc("CreateConsoleScreenBuffer") +) + +// Writer provides colorable Writer to the console +type Writer struct { + out io.Writer + handle syscall.Handle + althandle syscall.Handle + oldattr word + oldpos coord + rest bytes.Buffer + mutex sync.Mutex +} + +// NewColorable returns new instance of Writer which handles escape sequence from File. +func NewColorable(file *os.File) io.Writer { + if file == nil { + panic("nil passed instead of *os.File to NewColorable()") + } + + if isatty.IsTerminal(file.Fd()) { + var mode uint32 + if r, _, _ := procGetConsoleMode.Call(file.Fd(), uintptr(unsafe.Pointer(&mode))); r != 0 && mode&cENABLE_VIRTUAL_TERMINAL_PROCESSING != 0 { + return file + } + var csbi consoleScreenBufferInfo + handle := syscall.Handle(file.Fd()) + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + return &Writer{out: file, handle: handle, oldattr: csbi.attributes, oldpos: coord{0, 0}} + } + return file +} + +// NewColorableStdout returns new instance of Writer which handles escape sequence for stdout. +func NewColorableStdout() io.Writer { + return NewColorable(os.Stdout) +} + +// NewColorableStderr returns new instance of Writer which handles escape sequence for stderr. +func NewColorableStderr() io.Writer { + return NewColorable(os.Stderr) +} + +var color256 = map[int]int{ + 0: 0x000000, + 1: 0x800000, + 2: 0x008000, + 3: 0x808000, + 4: 0x000080, + 5: 0x800080, + 6: 0x008080, + 7: 0xc0c0c0, + 8: 0x808080, + 9: 0xff0000, + 10: 0x00ff00, + 11: 0xffff00, + 12: 0x0000ff, + 13: 0xff00ff, + 14: 0x00ffff, + 15: 0xffffff, + 16: 0x000000, + 17: 0x00005f, + 18: 0x000087, + 19: 0x0000af, + 20: 0x0000d7, + 21: 0x0000ff, + 22: 0x005f00, + 23: 0x005f5f, + 24: 0x005f87, + 25: 0x005faf, + 26: 0x005fd7, + 27: 0x005fff, + 28: 0x008700, + 29: 0x00875f, + 30: 0x008787, + 31: 0x0087af, + 32: 0x0087d7, + 33: 0x0087ff, + 34: 0x00af00, + 35: 0x00af5f, + 36: 0x00af87, + 37: 0x00afaf, + 38: 0x00afd7, + 39: 0x00afff, + 40: 0x00d700, + 41: 0x00d75f, + 42: 0x00d787, + 43: 0x00d7af, + 44: 0x00d7d7, + 45: 0x00d7ff, + 46: 0x00ff00, + 47: 0x00ff5f, + 48: 0x00ff87, + 49: 0x00ffaf, + 50: 0x00ffd7, + 51: 0x00ffff, + 52: 0x5f0000, + 53: 0x5f005f, + 54: 0x5f0087, + 55: 0x5f00af, + 56: 0x5f00d7, + 57: 0x5f00ff, + 58: 0x5f5f00, + 59: 0x5f5f5f, + 60: 0x5f5f87, + 61: 0x5f5faf, + 62: 0x5f5fd7, + 63: 0x5f5fff, + 64: 0x5f8700, + 65: 0x5f875f, + 66: 0x5f8787, + 67: 0x5f87af, + 68: 0x5f87d7, + 69: 0x5f87ff, + 70: 0x5faf00, + 71: 0x5faf5f, + 72: 0x5faf87, + 73: 0x5fafaf, + 74: 0x5fafd7, + 75: 0x5fafff, + 76: 0x5fd700, + 77: 0x5fd75f, + 78: 0x5fd787, + 79: 0x5fd7af, + 80: 0x5fd7d7, + 81: 0x5fd7ff, + 82: 0x5fff00, + 83: 0x5fff5f, + 84: 0x5fff87, + 85: 0x5fffaf, + 86: 0x5fffd7, + 87: 0x5fffff, + 88: 0x870000, + 89: 0x87005f, + 90: 0x870087, + 91: 0x8700af, + 92: 0x8700d7, + 93: 0x8700ff, + 94: 0x875f00, + 95: 0x875f5f, + 96: 0x875f87, + 97: 0x875faf, + 98: 0x875fd7, + 99: 0x875fff, + 100: 0x878700, + 101: 0x87875f, + 102: 0x878787, + 103: 0x8787af, + 104: 0x8787d7, + 105: 0x8787ff, + 106: 0x87af00, + 107: 0x87af5f, + 108: 0x87af87, + 109: 0x87afaf, + 110: 0x87afd7, + 111: 0x87afff, + 112: 0x87d700, + 113: 0x87d75f, + 114: 0x87d787, + 115: 0x87d7af, + 116: 0x87d7d7, + 117: 0x87d7ff, + 118: 0x87ff00, + 119: 0x87ff5f, + 120: 0x87ff87, + 121: 0x87ffaf, + 122: 0x87ffd7, + 123: 0x87ffff, + 124: 0xaf0000, + 125: 0xaf005f, + 126: 0xaf0087, + 127: 0xaf00af, + 128: 0xaf00d7, + 129: 0xaf00ff, + 130: 0xaf5f00, + 131: 0xaf5f5f, + 132: 0xaf5f87, + 133: 0xaf5faf, + 134: 0xaf5fd7, + 135: 0xaf5fff, + 136: 0xaf8700, + 137: 0xaf875f, + 138: 0xaf8787, + 139: 0xaf87af, + 140: 0xaf87d7, + 141: 0xaf87ff, + 142: 0xafaf00, + 143: 0xafaf5f, + 144: 0xafaf87, + 145: 0xafafaf, + 146: 0xafafd7, + 147: 0xafafff, + 148: 0xafd700, + 149: 0xafd75f, + 150: 0xafd787, + 151: 0xafd7af, + 152: 0xafd7d7, + 153: 0xafd7ff, + 154: 0xafff00, + 155: 0xafff5f, + 156: 0xafff87, + 157: 0xafffaf, + 158: 0xafffd7, + 159: 0xafffff, + 160: 0xd70000, + 161: 0xd7005f, + 162: 0xd70087, + 163: 0xd700af, + 164: 0xd700d7, + 165: 0xd700ff, + 166: 0xd75f00, + 167: 0xd75f5f, + 168: 0xd75f87, + 169: 0xd75faf, + 170: 0xd75fd7, + 171: 0xd75fff, + 172: 0xd78700, + 173: 0xd7875f, + 174: 0xd78787, + 175: 0xd787af, + 176: 0xd787d7, + 177: 0xd787ff, + 178: 0xd7af00, + 179: 0xd7af5f, + 180: 0xd7af87, + 181: 0xd7afaf, + 182: 0xd7afd7, + 183: 0xd7afff, + 184: 0xd7d700, + 185: 0xd7d75f, + 186: 0xd7d787, + 187: 0xd7d7af, + 188: 0xd7d7d7, + 189: 0xd7d7ff, + 190: 0xd7ff00, + 191: 0xd7ff5f, + 192: 0xd7ff87, + 193: 0xd7ffaf, + 194: 0xd7ffd7, + 195: 0xd7ffff, + 196: 0xff0000, + 197: 0xff005f, + 198: 0xff0087, + 199: 0xff00af, + 200: 0xff00d7, + 201: 0xff00ff, + 202: 0xff5f00, + 203: 0xff5f5f, + 204: 0xff5f87, + 205: 0xff5faf, + 206: 0xff5fd7, + 207: 0xff5fff, + 208: 0xff8700, + 209: 0xff875f, + 210: 0xff8787, + 211: 0xff87af, + 212: 0xff87d7, + 213: 0xff87ff, + 214: 0xffaf00, + 215: 0xffaf5f, + 216: 0xffaf87, + 217: 0xffafaf, + 218: 0xffafd7, + 219: 0xffafff, + 220: 0xffd700, + 221: 0xffd75f, + 222: 0xffd787, + 223: 0xffd7af, + 224: 0xffd7d7, + 225: 0xffd7ff, + 226: 0xffff00, + 227: 0xffff5f, + 228: 0xffff87, + 229: 0xffffaf, + 230: 0xffffd7, + 231: 0xffffff, + 232: 0x080808, + 233: 0x121212, + 234: 0x1c1c1c, + 235: 0x262626, + 236: 0x303030, + 237: 0x3a3a3a, + 238: 0x444444, + 239: 0x4e4e4e, + 240: 0x585858, + 241: 0x626262, + 242: 0x6c6c6c, + 243: 0x767676, + 244: 0x808080, + 245: 0x8a8a8a, + 246: 0x949494, + 247: 0x9e9e9e, + 248: 0xa8a8a8, + 249: 0xb2b2b2, + 250: 0xbcbcbc, + 251: 0xc6c6c6, + 252: 0xd0d0d0, + 253: 0xdadada, + 254: 0xe4e4e4, + 255: 0xeeeeee, +} + +// `\033]0;TITLESTR\007` +func doTitleSequence(er *bytes.Reader) error { + var c byte + var err error + + c, err = er.ReadByte() + if err != nil { + return err + } + if c != '0' && c != '2' { + return nil + } + c, err = er.ReadByte() + if err != nil { + return err + } + if c != ';' { + return nil + } + title := make([]byte, 0, 80) + for { + c, err = er.ReadByte() + if err != nil { + return err + } + if c == 0x07 || c == '\n' { + break + } + title = append(title, c) + } + if len(title) > 0 { + title8, err := syscall.UTF16PtrFromString(string(title)) + if err == nil { + procSetConsoleTitle.Call(uintptr(unsafe.Pointer(title8))) + } + } + return nil +} + +// returns Atoi(s) unless s == "" in which case it returns def +func atoiWithDefault(s string, def int) (int, error) { + if s == "" { + return def, nil + } + return strconv.Atoi(s) +} + +// Write writes data on console +func (w *Writer) Write(data []byte) (n int, err error) { + w.mutex.Lock() + defer w.mutex.Unlock() + var csbi consoleScreenBufferInfo + procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) + + handle := w.handle + + var er *bytes.Reader + if w.rest.Len() > 0 { + var rest bytes.Buffer + w.rest.WriteTo(&rest) + w.rest.Reset() + rest.Write(data) + er = bytes.NewReader(rest.Bytes()) + } else { + er = bytes.NewReader(data) + } + var bw [1]byte +loop: + for { + c1, err := er.ReadByte() + if err != nil { + break loop + } + if c1 != 0x1b { + bw[0] = c1 + w.out.Write(bw[:]) + continue + } + c2, err := er.ReadByte() + if err != nil { + break loop + } + + switch c2 { + case '>': + continue + case ']': + w.rest.WriteByte(c1) + w.rest.WriteByte(c2) + er.WriteTo(&w.rest) + if bytes.IndexByte(w.rest.Bytes(), 0x07) == -1 { + break loop + } + er = bytes.NewReader(w.rest.Bytes()[2:]) + err := doTitleSequence(er) + if err != nil { + break loop + } + w.rest.Reset() + continue + // https://github.com/mattn/go-colorable/issues/27 + case '7': + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + w.oldpos = csbi.cursorPosition + continue + case '8': + procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&w.oldpos))) + continue + case 0x5b: + // execute part after switch + default: + continue + } + + w.rest.WriteByte(c1) + w.rest.WriteByte(c2) + er.WriteTo(&w.rest) + + var buf bytes.Buffer + var m byte + for i, c := range w.rest.Bytes()[2:] { + if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' { + m = c + er = bytes.NewReader(w.rest.Bytes()[2+i+1:]) + w.rest.Reset() + break + } + buf.Write([]byte(string(c))) + } + if m == 0 { + break loop + } + + switch m { + case 'A': + n, err = atoiWithDefault(buf.String(), 1) + if err != nil { + continue + } + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + csbi.cursorPosition.y -= short(n) + procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) + case 'B': + n, err = atoiWithDefault(buf.String(), 1) + if err != nil { + continue + } + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + csbi.cursorPosition.y += short(n) + procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) + case 'C': + n, err = atoiWithDefault(buf.String(), 1) + if err != nil { + continue + } + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + csbi.cursorPosition.x += short(n) + procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) + case 'D': + n, err = atoiWithDefault(buf.String(), 1) + if err != nil { + continue + } + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + csbi.cursorPosition.x -= short(n) + if csbi.cursorPosition.x < 0 { + csbi.cursorPosition.x = 0 + } + procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) + case 'E': + n, err = strconv.Atoi(buf.String()) + if err != nil { + continue + } + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + csbi.cursorPosition.x = 0 + csbi.cursorPosition.y += short(n) + procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) + case 'F': + n, err = strconv.Atoi(buf.String()) + if err != nil { + continue + } + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + csbi.cursorPosition.x = 0 + csbi.cursorPosition.y -= short(n) + procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) + case 'G': + n, err = strconv.Atoi(buf.String()) + if err != nil { + continue + } + if n < 1 { + n = 1 + } + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + csbi.cursorPosition.x = short(n - 1) + procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) + case 'H', 'f': + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + if buf.Len() > 0 { + token := strings.Split(buf.String(), ";") + switch len(token) { + case 1: + n1, err := strconv.Atoi(token[0]) + if err != nil { + continue + } + csbi.cursorPosition.y = short(n1 - 1) + case 2: + n1, err := strconv.Atoi(token[0]) + if err != nil { + continue + } + n2, err := strconv.Atoi(token[1]) + if err != nil { + continue + } + csbi.cursorPosition.x = short(n2 - 1) + csbi.cursorPosition.y = short(n1 - 1) + } + } else { + csbi.cursorPosition.y = 0 + } + procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) + case 'J': + n := 0 + if buf.Len() > 0 { + n, err = strconv.Atoi(buf.String()) + if err != nil { + continue + } + } + var count, written dword + var cursor coord + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + switch n { + case 0: + cursor = coord{x: csbi.cursorPosition.x, y: csbi.cursorPosition.y} + count = dword(csbi.size.x) - dword(csbi.cursorPosition.x) + dword(csbi.size.y-csbi.cursorPosition.y)*dword(csbi.size.x) + case 1: + cursor = coord{x: csbi.window.left, y: csbi.window.top} + count = dword(csbi.size.x) - dword(csbi.cursorPosition.x) + dword(csbi.window.top-csbi.cursorPosition.y)*dword(csbi.size.x) + case 2: + cursor = coord{x: csbi.window.left, y: csbi.window.top} + count = dword(csbi.size.x) - dword(csbi.cursorPosition.x) + dword(csbi.size.y-csbi.cursorPosition.y)*dword(csbi.size.x) + } + procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) + procFillConsoleOutputAttribute.Call(uintptr(handle), uintptr(csbi.attributes), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) + case 'K': + n := 0 + if buf.Len() > 0 { + n, err = strconv.Atoi(buf.String()) + if err != nil { + continue + } + } + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + var cursor coord + var count, written dword + switch n { + case 0: + cursor = coord{x: csbi.cursorPosition.x, y: csbi.cursorPosition.y} + count = dword(csbi.size.x - csbi.cursorPosition.x) + case 1: + cursor = coord{x: csbi.window.left, y: csbi.cursorPosition.y} + count = dword(csbi.size.x - csbi.cursorPosition.x) + case 2: + cursor = coord{x: csbi.window.left, y: csbi.cursorPosition.y} + count = dword(csbi.size.x) + } + procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) + procFillConsoleOutputAttribute.Call(uintptr(handle), uintptr(csbi.attributes), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) + case 'X': + n := 0 + if buf.Len() > 0 { + n, err = strconv.Atoi(buf.String()) + if err != nil { + continue + } + } + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + var cursor coord + var written dword + cursor = coord{x: csbi.cursorPosition.x, y: csbi.cursorPosition.y} + procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(n), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) + procFillConsoleOutputAttribute.Call(uintptr(handle), uintptr(csbi.attributes), uintptr(n), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) + case 'm': + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + attr := csbi.attributes + cs := buf.String() + if cs == "" { + procSetConsoleTextAttribute.Call(uintptr(handle), uintptr(w.oldattr)) + continue + } + token := strings.Split(cs, ";") + for i := 0; i < len(token); i++ { + ns := token[i] + if n, err = strconv.Atoi(ns); err == nil { + switch { + case n == 0 || n == 100: + attr = w.oldattr + case n == 4: + attr |= commonLvbUnderscore + case (1 <= n && n <= 3) || n == 5: + attr |= foregroundIntensity + case n == 7 || n == 27: + attr = + (attr &^ (foregroundMask | backgroundMask)) | + ((attr & foregroundMask) << 4) | + ((attr & backgroundMask) >> 4) + case n == 22: + attr &^= foregroundIntensity + case n == 24: + attr &^= commonLvbUnderscore + case 30 <= n && n <= 37: + attr &= backgroundMask + if (n-30)&1 != 0 { + attr |= foregroundRed + } + if (n-30)&2 != 0 { + attr |= foregroundGreen + } + if (n-30)&4 != 0 { + attr |= foregroundBlue + } + case n == 38: // set foreground color. + if i < len(token)-2 && (token[i+1] == "5" || token[i+1] == "05") { + if n256, err := strconv.Atoi(token[i+2]); err == nil { + if n256foreAttr == nil { + n256setup() + } + attr &= backgroundMask + attr |= n256foreAttr[n256] + i += 2 + } + } else if len(token) == 5 && token[i+1] == "2" { + var r, g, b int + r, _ = strconv.Atoi(token[i+2]) + g, _ = strconv.Atoi(token[i+3]) + b, _ = strconv.Atoi(token[i+4]) + i += 4 + if r > 127 { + attr |= foregroundRed + } + if g > 127 { + attr |= foregroundGreen + } + if b > 127 { + attr |= foregroundBlue + } + } else { + attr = attr & (w.oldattr & backgroundMask) + } + case n == 39: // reset foreground color. + attr &= backgroundMask + attr |= w.oldattr & foregroundMask + case 40 <= n && n <= 47: + attr &= foregroundMask + if (n-40)&1 != 0 { + attr |= backgroundRed + } + if (n-40)&2 != 0 { + attr |= backgroundGreen + } + if (n-40)&4 != 0 { + attr |= backgroundBlue + } + case n == 48: // set background color. + if i < len(token)-2 && token[i+1] == "5" { + if n256, err := strconv.Atoi(token[i+2]); err == nil { + if n256backAttr == nil { + n256setup() + } + attr &= foregroundMask + attr |= n256backAttr[n256] + i += 2 + } + } else if len(token) == 5 && token[i+1] == "2" { + var r, g, b int + r, _ = strconv.Atoi(token[i+2]) + g, _ = strconv.Atoi(token[i+3]) + b, _ = strconv.Atoi(token[i+4]) + i += 4 + if r > 127 { + attr |= backgroundRed + } + if g > 127 { + attr |= backgroundGreen + } + if b > 127 { + attr |= backgroundBlue + } + } else { + attr = attr & (w.oldattr & foregroundMask) + } + case n == 49: // reset foreground color. + attr &= foregroundMask + attr |= w.oldattr & backgroundMask + case 90 <= n && n <= 97: + attr = (attr & backgroundMask) + attr |= foregroundIntensity + if (n-90)&1 != 0 { + attr |= foregroundRed + } + if (n-90)&2 != 0 { + attr |= foregroundGreen + } + if (n-90)&4 != 0 { + attr |= foregroundBlue + } + case 100 <= n && n <= 107: + attr = (attr & foregroundMask) + attr |= backgroundIntensity + if (n-100)&1 != 0 { + attr |= backgroundRed + } + if (n-100)&2 != 0 { + attr |= backgroundGreen + } + if (n-100)&4 != 0 { + attr |= backgroundBlue + } + } + procSetConsoleTextAttribute.Call(uintptr(handle), uintptr(attr)) + } + } + case 'h': + var ci consoleCursorInfo + cs := buf.String() + if cs == "5>" { + procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) + ci.visible = 0 + procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) + } else if cs == "?25" { + procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) + ci.visible = 1 + procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) + } else if cs == "?1049" { + if w.althandle == 0 { + h, _, _ := procCreateConsoleScreenBuffer.Call(uintptr(genericRead|genericWrite), 0, 0, uintptr(consoleTextmodeBuffer), 0, 0) + w.althandle = syscall.Handle(h) + if w.althandle != 0 { + handle = w.althandle + } + } + } + case 'l': + var ci consoleCursorInfo + cs := buf.String() + if cs == "5>" { + procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) + ci.visible = 1 + procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) + } else if cs == "?25" { + procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) + ci.visible = 0 + procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) + } else if cs == "?1049" { + if w.althandle != 0 { + syscall.CloseHandle(w.althandle) + w.althandle = 0 + handle = w.handle + } + } + case 's': + procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) + w.oldpos = csbi.cursorPosition + case 'u': + procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&w.oldpos))) + } + } + + return len(data), nil +} + +type consoleColor struct { + rgb int + red bool + green bool + blue bool + intensity bool +} + +func (c consoleColor) foregroundAttr() (attr word) { + if c.red { + attr |= foregroundRed + } + if c.green { + attr |= foregroundGreen + } + if c.blue { + attr |= foregroundBlue + } + if c.intensity { + attr |= foregroundIntensity + } + return +} + +func (c consoleColor) backgroundAttr() (attr word) { + if c.red { + attr |= backgroundRed + } + if c.green { + attr |= backgroundGreen + } + if c.blue { + attr |= backgroundBlue + } + if c.intensity { + attr |= backgroundIntensity + } + return +} + +var color16 = []consoleColor{ + {0x000000, false, false, false, false}, + {0x000080, false, false, true, false}, + {0x008000, false, true, false, false}, + {0x008080, false, true, true, false}, + {0x800000, true, false, false, false}, + {0x800080, true, false, true, false}, + {0x808000, true, true, false, false}, + {0xc0c0c0, true, true, true, false}, + {0x808080, false, false, false, true}, + {0x0000ff, false, false, true, true}, + {0x00ff00, false, true, false, true}, + {0x00ffff, false, true, true, true}, + {0xff0000, true, false, false, true}, + {0xff00ff, true, false, true, true}, + {0xffff00, true, true, false, true}, + {0xffffff, true, true, true, true}, +} + +type hsv struct { + h, s, v float32 +} + +func (a hsv) dist(b hsv) float32 { + dh := a.h - b.h + switch { + case dh > 0.5: + dh = 1 - dh + case dh < -0.5: + dh = -1 - dh + } + ds := a.s - b.s + dv := a.v - b.v + return float32(math.Sqrt(float64(dh*dh + ds*ds + dv*dv))) +} + +func toHSV(rgb int) hsv { + r, g, b := float32((rgb&0xFF0000)>>16)/256.0, + float32((rgb&0x00FF00)>>8)/256.0, + float32(rgb&0x0000FF)/256.0 + min, max := minmax3f(r, g, b) + h := max - min + if h > 0 { + if max == r { + h = (g - b) / h + if h < 0 { + h += 6 + } + } else if max == g { + h = 2 + (b-r)/h + } else { + h = 4 + (r-g)/h + } + } + h /= 6.0 + s := max - min + if max != 0 { + s /= max + } + v := max + return hsv{h: h, s: s, v: v} +} + +type hsvTable []hsv + +func toHSVTable(rgbTable []consoleColor) hsvTable { + t := make(hsvTable, len(rgbTable)) + for i, c := range rgbTable { + t[i] = toHSV(c.rgb) + } + return t +} + +func (t hsvTable) find(rgb int) consoleColor { + hsv := toHSV(rgb) + n := 7 + l := float32(5.0) + for i, p := range t { + d := hsv.dist(p) + if d < l { + l, n = d, i + } + } + return color16[n] +} + +func minmax3f(a, b, c float32) (min, max float32) { + if a < b { + if b < c { + return a, c + } else if a < c { + return a, b + } else { + return c, b + } + } else { + if a < c { + return b, c + } else if b < c { + return b, a + } else { + return c, a + } + } +} + +var n256foreAttr []word +var n256backAttr []word + +func n256setup() { + n256foreAttr = make([]word, 256) + n256backAttr = make([]word, 256) + t := toHSVTable(color16) + for i, rgb := range color256 { + c := t.find(rgb) + n256foreAttr[i] = c.foregroundAttr() + n256backAttr[i] = c.backgroundAttr() + } +} + +// EnableColorsStdout enable colors if possible. +func EnableColorsStdout(enabled *bool) func() { + var mode uint32 + h := os.Stdout.Fd() + if r, _, _ := procGetConsoleMode.Call(h, uintptr(unsafe.Pointer(&mode))); r != 0 { + if r, _, _ = procSetConsoleMode.Call(h, uintptr(mode|cENABLE_VIRTUAL_TERMINAL_PROCESSING)); r != 0 { + if enabled != nil { + *enabled = true + } + return func() { + procSetConsoleMode.Call(h, uintptr(mode)) + } + } + } + if enabled != nil { + *enabled = true + } + return func() {} +} diff --git a/utils/colorable/noncolorable.go b/utils/colorable/noncolorable.go new file mode 100644 index 00000000..95f2c6be --- /dev/null +++ b/utils/colorable/noncolorable.go @@ -0,0 +1,55 @@ +package colorable + +import ( + "bytes" + "io" +) + +// NonColorable holds writer but removes escape sequence. +type NonColorable struct { + out io.Writer +} + +// NewNonColorable returns new instance of Writer which removes escape sequence from Writer. +func NewNonColorable(w io.Writer) io.Writer { + return &NonColorable{out: w} +} + +// Write writes data on console +func (w *NonColorable) Write(data []byte) (n int, err error) { + er := bytes.NewReader(data) + var bw [1]byte +loop: + for { + c1, err := er.ReadByte() + if err != nil { + break loop + } + if c1 != 0x1b { + bw[0] = c1 + w.out.Write(bw[:]) + continue + } + c2, err := er.ReadByte() + if err != nil { + break loop + } + if c2 != 0x5b { + continue + } + + var buf bytes.Buffer + for { + c, err := er.ReadByte() + if err != nil { + break loop + } + if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' { + break + } + buf.Write([]byte(string(c))) + } + } + + return len(data), nil +} diff --git a/utils/encoding/LICENSE b/utils/encoding/LICENSE new file mode 100644 index 00000000..1fbffdf7 --- /dev/null +++ b/utils/encoding/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Segment.io, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/utils/encoding/ascii/valid.go b/utils/encoding/ascii/valid.go new file mode 100644 index 00000000..b16e0433 --- /dev/null +++ b/utils/encoding/ascii/valid.go @@ -0,0 +1,154 @@ +package ascii + +import ( + "unsafe" +) + +// Valid returns true if b contains only ASCII characters. +func Valid(b []byte) bool { + return valid(unsafe.Pointer(&b), uintptr(len(b))) +} + +// ValidString returns true if s contains only ASCII characters. +func ValidString(s string) bool { + return valid(unsafe.Pointer(&s), uintptr(len(s))) +} + +// ValidBytes returns true if b is an ASCII character. +func ValidByte(b byte) bool { + return b <= 0x7f +} + +// ValidBytes returns true if b is an ASCII character. +func ValidRune(r rune) bool { + return r <= 0x7f +} + +//go:nosplit +func valid(s unsafe.Pointer, n uintptr) bool { + i := uintptr(0) + p := *(*unsafe.Pointer)(s) + + for n >= 8 { + if ((*(*uint64)(unsafe.Pointer(uintptr(p) + i))) & 0x8080808080808080) != 0 { + return false + } + i += 8 + n -= 8 + } + + if n >= 4 { + if ((*(*uint32)(unsafe.Pointer(uintptr(p) + i))) & 0x80808080) != 0 { + return false + } + i += 4 + n -= 4 + } + + var x uint32 + switch n { + case 3: + x = uint32(*(*uint8)(unsafe.Pointer(uintptr(p) + i))) | uint32(*(*uint16)(unsafe.Pointer(uintptr(p) + i + 1)))<<8 + case 2: + x = uint32(*(*uint16)(unsafe.Pointer(uintptr(p) + i))) + case 1: + x = uint32(*(*uint8)(unsafe.Pointer(uintptr(p) + i))) + default: + return true + } + return (x & 0x80808080) == 0 +} + +// Valid returns true if b contains only printable ASCII characters. +func ValidPrint(b []byte) bool { + return validPrint(unsafe.Pointer(&b), uintptr(len(b))) +} + +// ValidString returns true if s contains only printable ASCII characters. +func ValidPrintString(s string) bool { + return validPrint(unsafe.Pointer(&s), uintptr(len(s))) +} + +// ValidBytes returns true if b is an ASCII character. +func ValidPrintByte(b byte) bool { + return 0x20 <= b && b <= 0x7e +} + +// ValidBytes returns true if b is an ASCII character. +func ValidPrintRune(r rune) bool { + return 0x20 <= r && r <= 0x7e +} + +//go:nosplit +func validPrint(s unsafe.Pointer, n uintptr) bool { + if n == 0 { + return true + } + + i := uintptr(0) + p := *(*unsafe.Pointer)(s) + + for (n - i) >= 8 { + x := *(*uint64)(unsafe.Pointer(uintptr(p) + i)) + if hasLess64(x, 0x20) || hasMore64(x, 0x7e) { + return false + } + i += 8 + } + + if (n - i) >= 4 { + x := *(*uint32)(unsafe.Pointer(uintptr(p) + i)) + if hasLess32(x, 0x20) || hasMore32(x, 0x7e) { + return false + } + i += 4 + } + + var x uint32 + switch n - i { + case 3: + x = 0x20000000 | uint32(*(*uint8)(unsafe.Pointer(uintptr(p) + i))) | uint32(*(*uint16)(unsafe.Pointer(uintptr(p) + i + 1)))<<8 + case 2: + x = 0x20200000 | uint32(*(*uint16)(unsafe.Pointer(uintptr(p) + i))) + case 1: + x = 0x20202000 | uint32(*(*uint8)(unsafe.Pointer(uintptr(p) + i))) + default: + return true + } + return !(hasLess32(x, 0x20) || hasMore32(x, 0x7e)) +} + +// https://graphics.stanford.edu/~seander/bithacks.html#HasLessInWord +const ( + hasLessConstL64 = (^uint64(0)) / 255 + hasLessConstR64 = hasLessConstL64 * 128 + + hasLessConstL32 = (^uint32(0)) / 255 + hasLessConstR32 = hasLessConstL32 * 128 + + hasMoreConstL64 = (^uint64(0)) / 255 + hasMoreConstR64 = hasMoreConstL64 * 128 + + hasMoreConstL32 = (^uint32(0)) / 255 + hasMoreConstR32 = hasMoreConstL32 * 128 +) + +//go:nosplit +func hasLess64(x, n uint64) bool { + return ((x - (hasLessConstL64 * n)) & ^x & hasLessConstR64) != 0 +} + +//go:nosplit +func hasLess32(x, n uint32) bool { + return ((x - (hasLessConstL32 * n)) & ^x & hasLessConstR32) != 0 +} + +//go:nosplit +func hasMore64(x, n uint64) bool { + return (((x + (hasMoreConstL64 * (127 - n))) | x) & hasMoreConstR64) != 0 +} + +//go:nosplit +func hasMore32(x, n uint32) bool { + return (((x + (hasMoreConstL32 * (127 - n))) | x) & hasMoreConstR32) != 0 +} diff --git a/utils/encoding/ascii/valid_test.go b/utils/encoding/ascii/valid_test.go new file mode 100644 index 00000000..50fe0720 --- /dev/null +++ b/utils/encoding/ascii/valid_test.go @@ -0,0 +1,92 @@ +package ascii + +import ( + "strings" + "testing" +) + +var testStrings = [...]string{ + "", + "hello", + "Hello World!", + "Hello\"World!", + "Hello\\World!", + "Hello\nWorld!", + "Hello\rWorld!", + "Hello\tWorld!", + "Hello\bWorld!", + "Hello\fWorld!", + "H~llo World!", + "H~llo", + "ไฝ ๅฅฝ", + "~", + "\x80", + "\x7F", + "\xFF", + "some kind of long string with only ascii characters.", + "some kind of long string with a non-ascii character at the end.\xff", + strings.Repeat("1234567890", 1000), +} + +func testString(s string, f func(byte) bool) bool { + for i := range s { + if !f(s[i]) { + return false + } + } + return true +} + +func testValid(s string) bool { + return testString(s, ValidByte) +} + +func testValidPrint(s string) bool { + return testString(s, ValidPrintByte) +} + +func TestValid(t *testing.T) { + testValidationFunction(t, testValid, ValidString) +} + +func TestValidPrint(t *testing.T) { + testValidationFunction(t, testValidPrint, ValidPrintString) +} + +func testValidationFunction(t *testing.T, reference, function func(string) bool) { + for _, test := range testStrings { + t.Run(limit(test), func(t *testing.T) { + expect := reference(test) + + if valid := function(test); expect != valid { + t.Errorf("expected %t but got %t", expect, valid) + } + }) + } +} + +func BenchmarkValid(b *testing.B) { + benchmarkValidationFunction(b, ValidString) +} + +func BenchmarkValidPrint(b *testing.B) { + benchmarkValidationFunction(b, ValidPrintString) +} + +func benchmarkValidationFunction(b *testing.B, function func(string) bool) { + for _, test := range testStrings { + b.Run(limit(test), func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = function(test) + } + b.SetBytes(int64(len(test))) + }) + } +} + +func limit(s string) string { + if len(s) > 17 { + return s[:17] + "..." + } + return s +} diff --git a/utils/encoding/iso8601/valid.go b/utils/encoding/iso8601/valid.go new file mode 100644 index 00000000..2c4edec7 --- /dev/null +++ b/utils/encoding/iso8601/valid.go @@ -0,0 +1,179 @@ +package iso8601 + +// ValidFlags is a bitset type used to configure the behavior of the Valid +//function. +type ValidFlags int + +const ( + // Strict is a validation flag used to represent a string iso8601 validation + // (this is the default). + Strict ValidFlags = 0 + + // AllowSpaceSeparator allows the presence of a space instead of a 'T' as + // separator between the date and time. + AllowSpaceSeparator ValidFlags = 1 << iota + + // AllowMissingTime allows the value to contain only a date. + AllowMissingTime + + // AllowMissingSubsecond allows the value to contain only a date and time. + AllowMissingSubsecond + + // AllowMissingTimezone allows the value to be missing the timezone + // information. + AllowMissingTimezone + + // AllowNumericTimezone allows the value to represent timezones in their + // numeric form. + AllowNumericTimezone + + // Flexible is a combination of all validation flag that allow for + // non-strict checking of the input value. + Flexible = AllowSpaceSeparator | AllowMissingTime | AllowMissingSubsecond | AllowMissingTimezone | AllowNumericTimezone +) + +// Valid check value to verify whether or not it is a valid iso8601 time +// representation. +func Valid(value string, flags ValidFlags) bool { + var ok bool + + // year + if value, ok = readDigits(value, 4, 4); !ok { + return false + } + + if value, ok = readByte(value, '-'); !ok { + return false + } + + // month + if value, ok = readDigits(value, 2, 2); !ok { + return false + } + + if value, ok = readByte(value, '-'); !ok { + return false + } + + // day + if value, ok = readDigits(value, 2, 2); !ok { + return false + } + + if len(value) == 0 && (flags&AllowMissingTime) != 0 { + return true // date only + } + + // separator + if value, ok = readByte(value, 'T'); !ok { + if (flags & AllowSpaceSeparator) == 0 { + return false + } + if value, ok = readByte(value, ' '); !ok { + return false + } + } + + // hour + if value, ok = readDigits(value, 2, 2); !ok { + return false + } + + if value, ok = readByte(value, ':'); !ok { + return false + } + + // minute + if value, ok = readDigits(value, 2, 2); !ok { + return false + } + + if value, ok = readByte(value, ':'); !ok { + return false + } + + // second + if value, ok = readDigits(value, 2, 2); !ok { + return false + } + + // microsecond + if value, ok = readByte(value, '.'); !ok { + if (flags & AllowMissingSubsecond) == 0 { + return false + } + } else { + if value, ok = readDigits(value, 1, 9); !ok { + return false + } + } + + if len(value) == 0 && (flags&AllowMissingTimezone) != 0 { + return true // date and time + } + + // timezone + if value, ok = readByte(value, 'Z'); ok { + return len(value) == 0 + } + + if (flags & AllowSpaceSeparator) != 0 { + value, _ = readByte(value, ' ') + } + + if value, ok = readByte(value, '+'); !ok { + if value, ok = readByte(value, '-'); !ok { + return false + } + } + + // timezone hour + if value, ok = readDigits(value, 2, 2); !ok { + return false + } + + if value, ok = readByte(value, ':'); !ok { + if (flags & AllowNumericTimezone) == 0 { + return false + } + } + + // timezone minute + if value, ok = readDigits(value, 2, 2); !ok { + return false + } + + return len(value) == 0 +} + +func readDigits(value string, min, max int) (string, bool) { + if len(value) < min { + return value, false + } + + i := 0 + + for i < max && i < len(value) && isDigit(value[i]) { + i++ + } + + if i < max && i < min { + return value, false + } + + return value[i:], true +} + +func readByte(value string, c byte) (string, bool) { + if len(value) == 0 { + return value, false + } + if value[0] != c { + return value, false + } + return value[1:], true +} + +func isDigit(c byte) bool { + return '0' <= c && c <= '9' +} diff --git a/utils/encoding/iso8601/valid_test.go b/utils/encoding/iso8601/valid_test.go new file mode 100644 index 00000000..acfbab11 --- /dev/null +++ b/utils/encoding/iso8601/valid_test.go @@ -0,0 +1,132 @@ +package iso8601 + +import ( + "testing" + "time" +) + +func TestValidate(t *testing.T) { + tests := []struct { + value string + flags ValidFlags + valid bool + }{ + // valid + {"2018-01-01T23:42:59.123456789Z", Strict, true}, + {"2018-01-01T23:42:59.123456789+07:00", Strict, true}, + {"2018-01-01T23:42:59.123456789-07:00", Strict, true}, + {"2018-01-01T23:42:59.000+07:00", Strict, true}, + + {"2018-01-01", Flexible, true}, + {"2018-01-01 23:42:59", Flexible, true}, + {"2018-01-01T23:42:59.123-0700", Flexible, true}, + + // invalid + {"", Flexible, false}, // empty string + {"whatever", Flexible, false}, // not a time + {"2018-01-01", Strict, false}, // missing time + {"2018-01-01 23:42:59-0700", Strict, false}, // missing subsecond + {"2018-01-01T23:42:59.123456789+0700", Strict, false}, // don't allow numeric time zone + {"2018_01-01T23:42:59.123456789Z", Strict, false}, // invalid date separator (first) + {"2018-01_01T23:42:59.123456789Z", Strict, false}, // invalid date separator (second) + {"2018-01-01 23:42:59.123456789Z", Strict, false}, // invalid date-time separator + {"2018-01-01T23-42:59.123456789Z", Strict, false}, // invalid time separator (first) + {"2018-01-01T23:42-59.123456789Z", Strict, false}, // invalid time separator (second) + {"2018-01-01T23:42:59,123456789Z", Strict, false}, // invalid decimal separator + {"2018-01-01T23:42:59.123456789", Strict, false}, // missing timezone + {"18-01-01T23:42:59.123456789Z", Strict, false}, // 2-digit year + {"2018-1-01T23:42:59.123456789Z", Strict, false}, // 1-digit month + {"2018-01-1T23:42:59.123456789Z", Strict, false}, // 1-digit day + {"2018-01-01T3:42:59.123456789Z", Strict, false}, // 1-digit hour + {"2018-01-01T23:2:59.123456789Z", Strict, false}, // 1-digit minute + {"2018-01-01T23:42:9.123456789Z", Strict, false}, // 1-digit second + {"2018-01-01T23:42:59.Z", Strict, false}, // not enough subsecond digits + {"2018-01-01T23:42:59.1234567890Z", Strict, false}, // too many subsecond digits + {"2018-01-01T23:42:59.123456789+7:00", Strict, false}, // 1-digit timezone hour + {"2018-01-01T23:42:59.123456789+07:0", Strict, false}, // 1-digit timezone minute + {"2018-01-01_23:42:59", Flexible, false}, // invalid date-time separator (not a space) + } + + for _, test := range tests { + if test.valid != Valid(test.value, test.flags) { + t.Errorf("%q expected Valid to return %t", test.value, test.valid) + } else if test.valid { + if !isIsoString(test.value) { + t.Errorf("behavior mismatch, isIsoString says %q must not be a valid date", test.value) + } + } else if test.flags != Strict { + if isIsoString(test.value) { + t.Errorf("behavior mismatch, isIsoString says %q must be a valid date", test.value) + } + } + } +} + +func BenchmarkValidate(b *testing.B) { + b.Run("success", benchmarkValidateSuccess) + b.Run("failure", benchmarkValidateFailure) +} + +func benchmarkValidateSuccess(b *testing.B) { + for i := 0; i < b.N; i++ { + if !Valid("2018-01-01T23:42:59.123456789Z", Flexible) { + b.Fatal("not valid") + } + } +} + +func benchmarkValidateFailure(b *testing.B) { + for i := 0; i < b.N; i++ { + if Valid("2018-01-01T23:42:59 oops!", Flexible) { + b.Fatal("valid but should not") + } + } +} + +func BenchmarkTimeParse(b *testing.B) { + b.Run("success", benchmarkTimeParseSuccess) + b.Run("failure", benchmarkTimeParseFailure) +} + +func benchmarkTimeParseSuccess(b *testing.B) { + for i := 0; i < b.N; i++ { + if _, err := time.Parse(time.RFC3339Nano, "2018-01-01T23:42:59.123456789Z"); err != nil { + b.Fatal("not valid") + } + } +} + +func benchmarkTimeParseFailure(b *testing.B) { + for i := 0; i < b.N; i++ { + if _, err := time.Parse(time.RFC3339Nano, "2018-01-01T23:42:59 oops!"); err == nil { + b.Fatal("valid but should not") + } + } +} + +// ============================================================================= +// This code is extracted from a library we had that we are replacing with this +// package, we use it to verify that the behavior matches. +// ============================================================================= +var validDates = [...]string{ + time.RFC3339Nano, + time.RFC3339, + "2006-01-02T15:04:05.999-0700", + "2006-01-02 15:04:05", + "2006-01-02", +} + +func isIsoString(str string) bool { + // Per RFC3339Nano Spec a date should never be more than 35 chars. + if len(str) > 36 { + return false + } + + for _, format := range validDates { + if _, err := time.Parse(format, str); err == nil { + return true + } + } + + return false +} diff --git a/utils/encoding/json/codec.go b/utils/encoding/json/codec.go new file mode 100644 index 00000000..cf7216d9 --- /dev/null +++ b/utils/encoding/json/codec.go @@ -0,0 +1,1179 @@ +package json + +import ( + "encoding" + "encoding/json" + "fmt" + "reflect" + "sort" + "strconv" + "strings" + "sync/atomic" + "time" + "unicode" + "unsafe" +) + +type codec struct { + encode encodeFunc + decode decodeFunc +} + +type encoder struct{ flags AppendFlags } +type decoder struct{ flags ParseFlags } + +type encodeFunc func(encoder, []byte, unsafe.Pointer) ([]byte, error) +type decodeFunc func(decoder, []byte, unsafe.Pointer) ([]byte, error) + +type emptyFunc func(unsafe.Pointer) bool +type sortFunc func([]reflect.Value) + +var ( + // Eventually consistent cache mapping go types to dynamically generated + // codecs. + // + // Note: using a uintptr as key instead of reflect.Type shaved ~15ns off of + // the ~30ns Marhsal/Unmarshal functions which were dominated by the map + // lookup time for simple types like bool, int, etc.. + cache unsafe.Pointer // map[unsafe.Pointer]codec +) + +func cacheLoad() map[unsafe.Pointer]codec { + p := atomic.LoadPointer(&cache) + return *(*map[unsafe.Pointer]codec)(unsafe.Pointer(&p)) +} + +func cacheStore(typ reflect.Type, cod codec, oldCodecs map[unsafe.Pointer]codec) { + newCodecs := make(map[unsafe.Pointer]codec, len(oldCodecs)+1) + newCodecs[typeid(typ)] = cod + + for t, c := range oldCodecs { + newCodecs[t] = c + } + + atomic.StorePointer(&cache, *(*unsafe.Pointer)(unsafe.Pointer(&newCodecs))) +} + +func typeid(t reflect.Type) unsafe.Pointer { + return (*iface)(unsafe.Pointer(&t)).ptr +} + +func constructCachedCodec(t reflect.Type, cache map[unsafe.Pointer]codec) codec { + c := constructCodec(t, map[reflect.Type]*structType{}, t.Kind() == reflect.Ptr) + + if inlined(t) { + c.encode = constructInlineValueEncodeFunc(c.encode) + } + + cacheStore(t, c, cache) + return c +} + +func constructCodec(t reflect.Type, seen map[reflect.Type]*structType, canAddr bool) (c codec) { + switch t { + case nullType, nil: + c = codec{encode: encoder.encodeNull, decode: decoder.decodeNull} + + case numberType: + c = codec{encode: encoder.encodeNumber, decode: decoder.decodeNumber} + + case bytesType: + c = codec{encode: encoder.encodeBytes, decode: decoder.decodeBytes} + + case durationType: + c = codec{encode: encoder.encodeDuration, decode: decoder.decodeDuration} + + case timeType: + c = codec{encode: encoder.encodeTime, decode: decoder.decodeTime} + + case interfaceType: + c = codec{encode: encoder.encodeInterface, decode: decoder.decodeInterface} + + case rawMessageType: + c = codec{encode: encoder.encodeRawMessage, decode: decoder.decodeRawMessage} + + case numberPtrType: + c = constructPointerCodec(numberPtrType, nil) + + case durationPtrType: + c = constructPointerCodec(durationPtrType, nil) + + case timePtrType: + c = constructPointerCodec(timePtrType, nil) + + case rawMessagePtrType: + c = constructPointerCodec(rawMessagePtrType, nil) + } + + if c.encode != nil { + return + } + + switch t.Kind() { + case reflect.Bool: + c = codec{encode: encoder.encodeBool, decode: decoder.decodeBool} + + case reflect.Int: + c = codec{encode: encoder.encodeInt, decode: decoder.decodeInt} + + case reflect.Int8: + c = codec{encode: encoder.encodeInt8, decode: decoder.decodeInt8} + + case reflect.Int16: + c = codec{encode: encoder.encodeInt16, decode: decoder.decodeInt16} + + case reflect.Int32: + c = codec{encode: encoder.encodeInt32, decode: decoder.decodeInt32} + + case reflect.Int64: + c = codec{encode: encoder.encodeInt64, decode: decoder.decodeInt64} + + case reflect.Uint: + c = codec{encode: encoder.encodeUint, decode: decoder.decodeUint} + + case reflect.Uintptr: + c = codec{encode: encoder.encodeUintptr, decode: decoder.decodeUintptr} + + case reflect.Uint8: + c = codec{encode: encoder.encodeUint8, decode: decoder.decodeUint8} + + case reflect.Uint16: + c = codec{encode: encoder.encodeUint16, decode: decoder.decodeUint16} + + case reflect.Uint32: + c = codec{encode: encoder.encodeUint32, decode: decoder.decodeUint32} + + case reflect.Uint64: + c = codec{encode: encoder.encodeUint64, decode: decoder.decodeUint64} + + case reflect.Float32: + c = codec{encode: encoder.encodeFloat32, decode: decoder.decodeFloat32} + + case reflect.Float64: + c = codec{encode: encoder.encodeFloat64, decode: decoder.decodeFloat64} + + case reflect.String: + c = codec{encode: encoder.encodeString, decode: decoder.decodeString} + + case reflect.Interface: + c = constructInterfaceCodec(t) + + case reflect.Array: + c = constructArrayCodec(t, seen, canAddr) + + case reflect.Slice: + c = constructSliceCodec(t, seen) + + case reflect.Map: + c = constructMapCodec(t, seen) + + case reflect.Struct: + c = constructStructCodec(t, seen, canAddr) + + case reflect.Ptr: + c = constructPointerCodec(t, seen) + + default: + c = constructUnsupportedTypeCodec(t) + } + + p := reflect.PtrTo(t) + + if canAddr { + switch { + case p.Implements(jsonMarshalerType): + c.encode = constructJSONMarshalerEncodeFunc(t, true) + case p.Implements(textMarshalerType): + c.encode = constructTextMarshalerEncodeFunc(t, true) + } + } + + switch { + case t.Implements(jsonMarshalerType): + c.encode = constructJSONMarshalerEncodeFunc(t, false) + case t.Implements(textMarshalerType): + c.encode = constructTextMarshalerEncodeFunc(t, false) + } + + switch { + case p.Implements(jsonUnmarshalerType): + c.decode = constructJSONUnmarshalerDecodeFunc(t, true) + case p.Implements(textUnmarshalerType): + c.decode = constructTextUnmarshalerDecodeFunc(t, true) + } + + return +} + +func constructStringCodec(t reflect.Type, seen map[reflect.Type]*structType, canAddr bool) codec { + c := constructCodec(t, seen, canAddr) + return codec{ + encode: constructStringEncodeFunc(c.encode), + decode: constructStringDecodeFunc(c.decode), + } +} + +func constructStringEncodeFunc(encode encodeFunc) encodeFunc { + return func(e encoder, b []byte, p unsafe.Pointer) ([]byte, error) { + return e.encodeToString(b, p, encode) + } +} + +func constructStringDecodeFunc(decode decodeFunc) decodeFunc { + return func(d decoder, b []byte, p unsafe.Pointer) ([]byte, error) { + return d.decodeFromString(b, p, decode) + } +} + +func constructStringToIntDecodeFunc(t reflect.Type, decode decodeFunc) decodeFunc { + return func(d decoder, b []byte, p unsafe.Pointer) ([]byte, error) { + return d.decodeFromStringToInt(b, p, t, decode) + } +} + +func constructArrayCodec(t reflect.Type, seen map[reflect.Type]*structType, canAddr bool) codec { + e := t.Elem() + c := constructCodec(e, seen, canAddr) + s := alignedSize(e) + return codec{ + encode: constructArrayEncodeFunc(s, t, c.encode), + decode: constructArrayDecodeFunc(s, t, c.decode), + } +} + +func constructArrayEncodeFunc(size uintptr, t reflect.Type, encode encodeFunc) encodeFunc { + n := t.Len() + return func(e encoder, b []byte, p unsafe.Pointer) ([]byte, error) { + return e.encodeArray(b, p, n, size, t, encode) + } +} + +func constructArrayDecodeFunc(size uintptr, t reflect.Type, decode decodeFunc) decodeFunc { + n := t.Len() + return func(d decoder, b []byte, p unsafe.Pointer) ([]byte, error) { + return d.decodeArray(b, p, n, size, t, decode) + } +} + +func constructSliceCodec(t reflect.Type, seen map[reflect.Type]*structType) codec { + e := t.Elem() + s := alignedSize(e) + + if e.Kind() == reflect.Uint8 { + // Go 1.7+ behavior: slices of byte types (and aliases) may override the + // default encoding and decoding behaviors by implementing marshaler and + // unmarshaler interfaces. + p := reflect.PtrTo(e) + c := codec{} + + switch { + case e.Implements(jsonMarshalerType): + c.encode = constructJSONMarshalerEncodeFunc(e, false) + case e.Implements(textMarshalerType): + c.encode = constructTextMarshalerEncodeFunc(e, false) + case p.Implements(jsonMarshalerType): + c.encode = constructJSONMarshalerEncodeFunc(e, true) + case p.Implements(textMarshalerType): + c.encode = constructTextMarshalerEncodeFunc(e, true) + } + + switch { + case e.Implements(jsonUnmarshalerType): + c.decode = constructJSONUnmarshalerDecodeFunc(e, false) + case e.Implements(textUnmarshalerType): + c.decode = constructTextUnmarshalerDecodeFunc(e, false) + case p.Implements(jsonUnmarshalerType): + c.decode = constructJSONUnmarshalerDecodeFunc(e, true) + case p.Implements(textUnmarshalerType): + c.decode = constructTextUnmarshalerDecodeFunc(e, true) + } + + if c.encode != nil { + c.encode = constructSliceEncodeFunc(s, t, c.encode) + } else { + c.encode = encoder.encodeBytes + } + + if c.decode != nil { + c.decode = constructSliceDecodeFunc(s, t, c.decode) + } else { + c.decode = decoder.decodeBytes + } + + return c + } + + c := constructCodec(e, seen, true) + return codec{ + encode: constructSliceEncodeFunc(s, t, c.encode), + decode: constructSliceDecodeFunc(s, t, c.decode), + } +} + +func constructSliceEncodeFunc(size uintptr, t reflect.Type, encode encodeFunc) encodeFunc { + return func(e encoder, b []byte, p unsafe.Pointer) ([]byte, error) { + return e.encodeSlice(b, p, size, t, encode) + } +} + +func constructSliceDecodeFunc(size uintptr, t reflect.Type, decode decodeFunc) decodeFunc { + return func(d decoder, b []byte, p unsafe.Pointer) ([]byte, error) { + return d.decodeSlice(b, p, size, t, decode) + } +} + +func constructMapCodec(t reflect.Type, seen map[reflect.Type]*structType) codec { + var sortKeys sortFunc + k := t.Key() + v := t.Elem() + + // Faster implementations for some common cases. + switch { + case k == stringType && v == interfaceType: + return codec{ + encode: encoder.encodeMapStringInterface, + decode: decoder.decodeMapStringInterface, + } + + case k == stringType && v == rawMessageType: + return codec{ + encode: encoder.encodeMapStringRawMessage, + decode: decoder.decodeMapStringRawMessage, + } + } + + kc := codec{} + vc := constructCodec(v, seen, false) + + if k.Implements(textMarshalerType) || reflect.PtrTo(k).Implements(textUnmarshalerType) { + kc.encode = constructTextMarshalerEncodeFunc(k, false) + kc.decode = constructTextUnmarshalerDecodeFunc(k, true) + + sortKeys = func(keys []reflect.Value) { + sort.Slice(keys, func(i, j int) bool { + // This is a performance abomination but the use case is rare + // enough that it shouldn't be a problem in practice. + k1, _ := keys[i].Interface().(encoding.TextMarshaler).MarshalText() + k2, _ := keys[j].Interface().(encoding.TextMarshaler).MarshalText() + return string(k1) < string(k2) + }) + } + } else { + switch k.Kind() { + case reflect.String: + kc.encode = encoder.encodeString + kc.decode = decoder.decodeString + + sortKeys = func(keys []reflect.Value) { + sort.Slice(keys, func(i, j int) bool { return keys[i].String() < keys[j].String() }) + } + + case reflect.Int, + reflect.Int8, + reflect.Int16, + reflect.Int32, + reflect.Int64: + kc = constructStringCodec(k, seen, false) + + sortKeys = func(keys []reflect.Value) { + sort.Slice(keys, func(i, j int) bool { return intStringsAreSorted(keys[i].Int(), keys[j].Int()) }) + } + + case reflect.Uint, + reflect.Uintptr, + reflect.Uint8, + reflect.Uint16, + reflect.Uint32, + reflect.Uint64: + kc = constructStringCodec(k, seen, false) + + sortKeys = func(keys []reflect.Value) { + sort.Slice(keys, func(i, j int) bool { return uintStringsAreSorted(keys[i].Uint(), keys[j].Uint()) }) + } + + default: + return constructUnsupportedTypeCodec(t) + } + } + + if inlined(v) { + vc.encode = constructInlineValueEncodeFunc(vc.encode) + } + + return codec{ + encode: constructMapEncodeFunc(t, kc.encode, vc.encode, sortKeys), + decode: constructMapDecodeFunc(t, kc.decode, vc.decode), + } +} + +func constructMapEncodeFunc(t reflect.Type, encodeKey, encodeValue encodeFunc, sortKeys sortFunc) encodeFunc { + return func(e encoder, b []byte, p unsafe.Pointer) ([]byte, error) { + return e.encodeMap(b, p, t, encodeKey, encodeValue, sortKeys) + } +} + +func constructMapDecodeFunc(t reflect.Type, decodeKey, decodeValue decodeFunc) decodeFunc { + kt := t.Key() + vt := t.Elem() + kz := reflect.Zero(kt) + vz := reflect.Zero(vt) + return func(d decoder, b []byte, p unsafe.Pointer) ([]byte, error) { + return d.decodeMap(b, p, t, kt, vt, kz, vz, decodeKey, decodeValue) + } +} + +func constructStructCodec(t reflect.Type, seen map[reflect.Type]*structType, canAddr bool) codec { + st := constructStructType(t, seen, canAddr) + return codec{ + encode: constructStructEncodeFunc(st), + decode: constructStructDecodeFunc(st), + } +} + +func constructStructType(t reflect.Type, seen map[reflect.Type]*structType, canAddr bool) *structType { + // Used for preventing infinite recursion on types that have pointers to + // themselves. + st := seen[t] + + if st == nil { + st = &structType{ + fields: make([]structField, 0, t.NumField()), + fieldsIndex: make(map[string]*structField), + ficaseIndex: make(map[string]*structField), + typ: t, + } + + seen[t] = st + st.fields = appendStructFields(st.fields, t, 0, seen, canAddr) + + for i := range st.fields { + f := &st.fields[i] + s := strings.ToLower(f.name) + st.fieldsIndex[f.name] = f + // When there is ambiguity because multiple fields have the same + // case-insensitive representation, the first field must win. + if _, exists := st.ficaseIndex[s]; !exists { + st.ficaseIndex[s] = f + } + } + } + + return st +} + +func constructStructEncodeFunc(st *structType) encodeFunc { + return func(e encoder, b []byte, p unsafe.Pointer) ([]byte, error) { + return e.encodeStruct(b, p, st) + } +} + +func constructStructDecodeFunc(st *structType) decodeFunc { + return func(d decoder, b []byte, p unsafe.Pointer) ([]byte, error) { + return d.decodeStruct(b, p, st) + } +} + +func constructEmbeddedStructPointerCodec(t reflect.Type, unexported bool, offset uintptr, field codec) codec { + return codec{ + encode: constructEmbeddedStructPointerEncodeFunc(t, unexported, offset, field.encode), + decode: constructEmbeddedStructPointerDecodeFunc(t, unexported, offset, field.decode), + } +} + +func constructEmbeddedStructPointerEncodeFunc(t reflect.Type, unexported bool, offset uintptr, encode encodeFunc) encodeFunc { + return func(e encoder, b []byte, p unsafe.Pointer) ([]byte, error) { + return e.encodeEmbeddedStructPointer(b, p, t, unexported, offset, encode) + } +} + +func constructEmbeddedStructPointerDecodeFunc(t reflect.Type, unexported bool, offset uintptr, decode decodeFunc) decodeFunc { + return func(d decoder, b []byte, p unsafe.Pointer) ([]byte, error) { + return d.decodeEmbeddedStructPointer(b, p, t, unexported, offset, decode) + } +} + +func appendStructFields(fields []structField, t reflect.Type, offset uintptr, seen map[reflect.Type]*structType, canAddr bool) []structField { + type embeddedField struct { + index int + offset uintptr + pointer bool + unexported bool + subtype *structType + subfield *structField + } + + names := make(map[string]struct{}) + embedded := make([]embeddedField, 0, 10) + + for i, n := 0, t.NumField(); i < n; i++ { + f := t.Field(i) + + var ( + name = f.Name + anonymous = f.Anonymous + tag = false + omitempty = false + stringify = false + unexported = len(f.PkgPath) != 0 + ) + + if unexported && !anonymous { // unexported + continue + } + + if parts := strings.Split(f.Tag.Get("json"), ","); len(parts) != 0 { + if len(parts[0]) != 0 { + name, tag = parts[0], true + } + + if name == "-" && len(parts) == 1 { // ignored + continue + } + + if !isValidTag(name) { + name = f.Name + } + + for _, tag := range parts[1:] { + switch tag { + case "omitempty": + omitempty = true + case "string": + stringify = true + } + } + } + + if anonymous && !tag { // embedded + typ := f.Type + ptr := f.Type.Kind() == reflect.Ptr + + if ptr { + typ = f.Type.Elem() + } + + if typ.Kind() == reflect.Struct { + // When the embedded fields is inlined the fields can be looked + // up by offset from the address of the wrapping object, so we + // simply add the embedded struct fields to the list of fields + // of the current struct type. + subtype := constructStructType(typ, seen, canAddr) + + for j := range subtype.fields { + embedded = append(embedded, embeddedField{ + index: i<<32 | j, + offset: offset + f.Offset, + pointer: ptr, + unexported: unexported, + subtype: subtype, + subfield: &subtype.fields[j], + }) + } + + continue + } + + if unexported { // ignore unexported non-struct types + continue + } + } + + codec := constructCodec(f.Type, seen, canAddr) + + if stringify { + // https://golang.org/pkg/encoding/json/#Marshal + // + // The "string" option signals that a field is stored as JSON inside + // a JSON-encoded string. It applies only to fields of string, + // floating point, integer, or boolean types. This extra level of + // encoding is sometimes used when communicating with JavaScript + // programs: + typ := f.Type + + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + } + + switch typ.Kind() { + case reflect.Int, + reflect.Int8, + reflect.Int16, + reflect.Int32, + reflect.Int64, + reflect.Uint, + reflect.Uintptr, + reflect.Uint8, + reflect.Uint16, + reflect.Uint32, + reflect.Uint64: + codec.encode = constructStringEncodeFunc(codec.encode) + codec.decode = constructStringToIntDecodeFunc(typ, codec.decode) + case reflect.Bool, + reflect.Float32, + reflect.Float64, + reflect.String: + codec.encode = constructStringEncodeFunc(codec.encode) + codec.decode = constructStringDecodeFunc(codec.decode) + } + } + + fields = append(fields, structField{ + codec: codec, + offset: offset + f.Offset, + empty: emptyFuncOf(f.Type), + tag: tag, + omitempty: omitempty, + name: name, + index: i << 32, + typ: f.Type, + zero: reflect.Zero(f.Type), + }) + + names[name] = struct{}{} + } + + // Only unambiguous embedded fields must be serialized. + ambiguousNames := make(map[string]int) + ambiguousTags := make(map[string]int) + + // Embedded types can never override a field that was already present at + // the top-level. + for name := range names { + ambiguousNames[name]++ + ambiguousTags[name]++ + } + + for _, embfield := range embedded { + ambiguousNames[embfield.subfield.name]++ + if embfield.subfield.tag { + ambiguousTags[embfield.subfield.name]++ + } + } + + for _, embfield := range embedded { + subfield := *embfield.subfield + + if ambiguousNames[subfield.name] > 1 && !(subfield.tag && ambiguousTags[subfield.name] == 1) { + continue // ambiguous embedded field + } + + if embfield.pointer { + subfield.codec = constructEmbeddedStructPointerCodec(embfield.subtype.typ, embfield.unexported, subfield.offset, subfield.codec) + subfield.offset = embfield.offset + } else { + subfield.offset += embfield.offset + } + + // To prevent dominant flags more than one level below the embedded one. + subfield.tag = false + + // To ensure the order of the fields in the output is the same is in the + // struct type. + subfield.index = embfield.index + + fields = append(fields, subfield) + } + + for i := range fields { + fields[i].json = encodeString(fields[i].name, 0) + fields[i].html = encodeString(fields[i].name, EscapeHTML) + } + + sort.Slice(fields, func(i, j int) bool { return fields[i].index < fields[j].index }) + return fields +} + +func encodeString(s string, flags AppendFlags) string { + b := make([]byte, 0, len(s)+2) + e := encoder{flags: flags} + b, _ = e.encodeString(b, unsafe.Pointer(&s)) + return *(*string)(unsafe.Pointer(&b)) +} + +func constructPointerCodec(t reflect.Type, seen map[reflect.Type]*structType) codec { + e := t.Elem() + c := constructCodec(e, seen, true) + return codec{ + encode: constructPointerEncodeFunc(e, c.encode), + decode: constructPointerDecodeFunc(e, c.decode), + } +} + +func constructPointerEncodeFunc(t reflect.Type, encode encodeFunc) encodeFunc { + return func(e encoder, b []byte, p unsafe.Pointer) ([]byte, error) { + return e.encodePointer(b, p, t, encode) + } +} + +func constructPointerDecodeFunc(t reflect.Type, decode decodeFunc) decodeFunc { + return func(d decoder, b []byte, p unsafe.Pointer) ([]byte, error) { + return d.decodePointer(b, p, t, decode) + } +} + +func constructInterfaceCodec(t reflect.Type) codec { + return codec{ + encode: constructMaybeEmptyInterfaceEncoderFunc(t), + decode: constructMaybeEmptyInterfaceDecoderFunc(t), + } +} + +func constructMaybeEmptyInterfaceEncoderFunc(t reflect.Type) encodeFunc { + return func(e encoder, b []byte, p unsafe.Pointer) ([]byte, error) { + return e.encodeMaybeEmptyInterface(b, p, t) + } +} + +func constructMaybeEmptyInterfaceDecoderFunc(t reflect.Type) decodeFunc { + return func(d decoder, b []byte, p unsafe.Pointer) ([]byte, error) { + return d.decodeMaybeEmptyInterface(b, p, t) + } +} + +func constructUnsupportedTypeCodec(t reflect.Type) codec { + return codec{ + encode: constructUnsupportedTypeEncodeFunc(t), + decode: constructUnsupportedTypeDecodeFunc(t), + } +} + +func constructUnsupportedTypeEncodeFunc(t reflect.Type) encodeFunc { + return func(e encoder, b []byte, p unsafe.Pointer) ([]byte, error) { + return e.encodeUnsupportedTypeError(b, p, t) + } +} + +func constructUnsupportedTypeDecodeFunc(t reflect.Type) decodeFunc { + return func(d decoder, b []byte, p unsafe.Pointer) ([]byte, error) { + return d.decodeUnmarshalTypeError(b, p, t) + } +} + +func constructJSONMarshalerEncodeFunc(t reflect.Type, pointer bool) encodeFunc { + return func(e encoder, b []byte, p unsafe.Pointer) ([]byte, error) { + return e.encodeJSONMarshaler(b, p, t, pointer) + } +} + +func constructJSONUnmarshalerDecodeFunc(t reflect.Type, pointer bool) decodeFunc { + return func(d decoder, b []byte, p unsafe.Pointer) ([]byte, error) { + return d.decodeJSONUnmarshaler(b, p, t, pointer) + } +} + +func constructTextMarshalerEncodeFunc(t reflect.Type, pointer bool) encodeFunc { + return func(e encoder, b []byte, p unsafe.Pointer) ([]byte, error) { + return e.encodeTextMarshaler(b, p, t, pointer) + } +} + +func constructTextUnmarshalerDecodeFunc(t reflect.Type, pointer bool) decodeFunc { + return func(d decoder, b []byte, p unsafe.Pointer) ([]byte, error) { + return d.decodeTextUnmarshaler(b, p, t, pointer) + } +} + +func constructInlineValueEncodeFunc(encode encodeFunc) encodeFunc { + return func(e encoder, b []byte, p unsafe.Pointer) ([]byte, error) { + return encode(e, b, noescape(unsafe.Pointer(&p))) + } +} + +// noescape hides a pointer from escape analysis. noescape is +// the identity function but escape analysis doesn't think the +// output depends on the input. noescape is inlined and currently +// compiles down to zero instructions. +// USE CAREFULLY! +// This was copied from the runtime; see issues 23382 and 7921. +//go:nosplit +func noescape(p unsafe.Pointer) unsafe.Pointer { + x := uintptr(p) + return unsafe.Pointer(x ^ 0) +} + +func alignedSize(t reflect.Type) uintptr { + a := t.Align() + s := t.Size() + return align(uintptr(a), uintptr(s)) +} + +func align(align, size uintptr) uintptr { + if align != 0 && (size%align) != 0 { + size = ((size / align) + 1) * align + } + return size +} + +func inlined(t reflect.Type) bool { + switch t.Kind() { + case reflect.Ptr: + return true + case reflect.Map: + return true + case reflect.Struct: + return t.NumField() == 1 && inlined(t.Field(0).Type) + default: + return false + } +} + +func isValidTag(s string) bool { + if s == "" { + return false + } + for _, c := range s { + switch { + case strings.ContainsRune("!#$%&()*+-./:<=>?@[]^_{|}~ ", c): + // Backslash and quote chars are reserved, but + // otherwise any punctuation chars are allowed + // in a tag name. + default: + if !unicode.IsLetter(c) && !unicode.IsDigit(c) { + return false + } + } + } + return true +} + +func emptyFuncOf(t reflect.Type) emptyFunc { + switch t { + case bytesType, rawMessageType: + return func(p unsafe.Pointer) bool { return (*slice)(p).len == 0 } + } + + switch t.Kind() { + case reflect.Array: + if t.Len() == 0 { + return func(unsafe.Pointer) bool { return true } + } + + case reflect.Map: + return func(p unsafe.Pointer) bool { return reflect.NewAt(t, p).Elem().Len() == 0 } + + case reflect.Slice: + return func(p unsafe.Pointer) bool { return (*slice)(p).len == 0 } + + case reflect.String: + return func(p unsafe.Pointer) bool { return len(*(*string)(p)) == 0 } + + case reflect.Bool: + return func(p unsafe.Pointer) bool { return !*(*bool)(p) } + + case reflect.Int, reflect.Uint: + return func(p unsafe.Pointer) bool { return *(*uint)(p) == 0 } + + case reflect.Uintptr: + return func(p unsafe.Pointer) bool { return *(*uintptr)(p) == 0 } + + case reflect.Int8, reflect.Uint8: + return func(p unsafe.Pointer) bool { return *(*uint8)(p) == 0 } + + case reflect.Int16, reflect.Uint16: + return func(p unsafe.Pointer) bool { return *(*uint16)(p) == 0 } + + case reflect.Int32, reflect.Uint32: + return func(p unsafe.Pointer) bool { return *(*uint32)(p) == 0 } + + case reflect.Int64, reflect.Uint64: + return func(p unsafe.Pointer) bool { return *(*uint64)(p) == 0 } + + case reflect.Float32: + return func(p unsafe.Pointer) bool { return *(*float32)(p) == 0 } + + case reflect.Float64: + return func(p unsafe.Pointer) bool { return *(*float64)(p) == 0 } + + case reflect.Ptr: + return func(p unsafe.Pointer) bool { return *(*unsafe.Pointer)(p) == nil } + + case reflect.Interface: + return func(p unsafe.Pointer) bool { return (*iface)(p).ptr == nil } + } + + return func(unsafe.Pointer) bool { return false } +} + +type iface struct { + typ unsafe.Pointer + ptr unsafe.Pointer +} + +type slice struct { + data unsafe.Pointer + len int + cap int +} + +type structType struct { + fields []structField + fieldsIndex map[string]*structField + ficaseIndex map[string]*structField + typ reflect.Type + inlined bool +} + +type structField struct { + codec codec + offset uintptr + empty emptyFunc + tag bool + omitempty bool + json string + html string + name string + typ reflect.Type + zero reflect.Value + index int +} + +func unmarshalTypeError(b []byte, t reflect.Type) error { + return &UnmarshalTypeError{Value: strconv.Quote(prefix(b)), Type: t} +} + +func unmarshalOverflow(b []byte, t reflect.Type) error { + return &UnmarshalTypeError{Value: "number " + prefix(b) + " overflows", Type: t} +} + +func unexpectedEOF(b []byte) error { + return syntaxError(b, "unexpected end of JSON input") +} + +var syntaxErrorMsgOffset = ^uintptr(0) + +func init() { + t := reflect.TypeOf(SyntaxError{}) + for i, n := 0, t.NumField(); i < n; i++ { + if f := t.Field(i); f.Type.Kind() == reflect.String { + syntaxErrorMsgOffset = f.Offset + } + } +} + +func syntaxError(b []byte, msg string, args ...interface{}) error { + e := new(SyntaxError) + i := syntaxErrorMsgOffset + if i != ^uintptr(0) { + s := "json: " + fmt.Sprintf(msg, args...) + ": " + prefix(b) + p := unsafe.Pointer(e) + // Hack to set the unexported `msg` field. + *(*string)(unsafe.Pointer(uintptr(p) + i)) = s + } + return e +} + +func inputError(b []byte, t reflect.Type) ([]byte, error) { + if len(b) == 0 { + return nil, unexpectedEOF(b) + } + _, r, err := parseValue(b) + if err != nil { + return r, err + } + return skipSpaces(r), unmarshalTypeError(b, t) +} + +func objectKeyError(b []byte, err error) ([]byte, error) { + if len(b) == 0 { + return nil, unexpectedEOF(b) + } + switch err.(type) { + case *UnmarshalTypeError: + err = syntaxError(b, "invalid character '%c' looking for beginning of object key", b[0]) + } + return b, err +} + +func prefix(b []byte) string { + if len(b) < 32 { + return string(b) + } + return string(b[:32]) + "..." +} + +func intStringsAreSorted(i0, i1 int64) bool { + var b0, b1 [32]byte + return string(strconv.AppendInt(b0[:0], i0, 10)) < string(strconv.AppendInt(b1[:0], i1, 10)) +} + +func uintStringsAreSorted(u0, u1 uint64) bool { + var b0, b1 [32]byte + return string(strconv.AppendUint(b0[:0], u0, 10)) < string(strconv.AppendUint(b1[:0], u1, 10)) +} + +//go:nosplit +func stringToBytes(s string) []byte { + return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{ + Data: ((*reflect.StringHeader)(unsafe.Pointer(&s))).Data, + Len: len(s), + Cap: len(s), + })) +} + +var ( + nullType = reflect.TypeOf(nil) + boolType = reflect.TypeOf(false) + + intType = reflect.TypeOf(int(0)) + int8Type = reflect.TypeOf(int8(0)) + int16Type = reflect.TypeOf(int16(0)) + int32Type = reflect.TypeOf(int32(0)) + int64Type = reflect.TypeOf(int64(0)) + + uintType = reflect.TypeOf(uint(0)) + uint8Type = reflect.TypeOf(uint8(0)) + uint16Type = reflect.TypeOf(uint16(0)) + uint32Type = reflect.TypeOf(uint32(0)) + uint64Type = reflect.TypeOf(uint64(0)) + uintptrType = reflect.TypeOf(uintptr(0)) + + float32Type = reflect.TypeOf(float32(0)) + float64Type = reflect.TypeOf(float64(0)) + + numberType = reflect.TypeOf(json.Number("")) + stringType = reflect.TypeOf("") + bytesType = reflect.TypeOf(([]byte)(nil)) + durationType = reflect.TypeOf(time.Duration(0)) + timeType = reflect.TypeOf(time.Time{}) + rawMessageType = reflect.TypeOf(RawMessage(nil)) + + numberPtrType = reflect.PtrTo(numberType) + durationPtrType = reflect.PtrTo(durationType) + timePtrType = reflect.PtrTo(timeType) + rawMessagePtrType = reflect.PtrTo(rawMessageType) + + sliceInterfaceType = reflect.TypeOf(([]interface{})(nil)) + mapStringInterfaceType = reflect.TypeOf((map[string]interface{})(nil)) + mapStringRawMessageType = reflect.TypeOf((map[string]RawMessage)(nil)) + + interfaceType = reflect.TypeOf((*interface{})(nil)).Elem() + jsonMarshalerType = reflect.TypeOf((*Marshaler)(nil)).Elem() + jsonUnmarshalerType = reflect.TypeOf((*Unmarshaler)(nil)).Elem() + textMarshalerType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem() + textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() +) + +// ============================================================================= +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// appendDuration appends a human-readable representation of d to b. +// +// The function copies the implementation of time.Duration.String but prevents +// Go from making a dynamic memory allocation on the returned value. +func appendDuration(b []byte, d time.Duration) []byte { + // Largest time is 2540400h10m10.000000000s + var buf [32]byte + w := len(buf) + + u := uint64(d) + neg := d < 0 + if neg { + u = -u + } + + if u < uint64(time.Second) { + // Special case: if duration is smaller than a second, + // use smaller units, like 1.2ms + var prec int + w-- + buf[w] = 's' + w-- + switch { + case u == 0: + return append(b, '0', 's') + case u < uint64(time.Microsecond): + // print nanoseconds + prec = 0 + buf[w] = 'n' + case u < uint64(time.Millisecond): + // print microseconds + prec = 3 + // U+00B5 'ยต' micro sign == 0xC2 0xB5 + w-- // Need room for two bytes. + copy(buf[w:], "ยต") + default: + // print milliseconds + prec = 6 + buf[w] = 'm' + } + w, u = fmtFrac(buf[:w], u, prec) + w = fmtInt(buf[:w], u) + } else { + w-- + buf[w] = 's' + + w, u = fmtFrac(buf[:w], u, 9) + + // u is now integer seconds + w = fmtInt(buf[:w], u%60) + u /= 60 + + // u is now integer minutes + if u > 0 { + w-- + buf[w] = 'm' + w = fmtInt(buf[:w], u%60) + u /= 60 + + // u is now integer hours + // Stop at hours because days can be different lengths. + if u > 0 { + w-- + buf[w] = 'h' + w = fmtInt(buf[:w], u) + } + } + } + + if neg { + w-- + buf[w] = '-' + } + + return append(b, buf[w:]...) +} + +// fmtFrac formats the fraction of v/10**prec (e.g., ".12345") into the +// tail of buf, omitting trailing zeros. it omits the decimal +// point too when the fraction is 0. It returns the index where the +// output bytes begin and the value v/10**prec. +func fmtFrac(buf []byte, v uint64, prec int) (nw int, nv uint64) { + // Omit trailing zeros up to and including decimal point. + w := len(buf) + print := false + for i := 0; i < prec; i++ { + digit := v % 10 + print = print || digit != 0 + if print { + w-- + buf[w] = byte(digit) + '0' + } + v /= 10 + } + if print { + w-- + buf[w] = '.' + } + return w, v +} + +// fmtInt formats v into the tail of buf. +// It returns the index where the output begins. +func fmtInt(buf []byte, v uint64) int { + w := len(buf) + if v == 0 { + w-- + buf[w] = '0' + } else { + for v > 0 { + w-- + buf[w] = byte(v%10) + '0' + v /= 10 + } + } + return w +} + +// ============================================================================= diff --git a/utils/encoding/json/decode.go b/utils/encoding/json/decode.go new file mode 100644 index 00000000..caf6e8a5 --- /dev/null +++ b/utils/encoding/json/decode.go @@ -0,0 +1,1192 @@ +package json + +import ( + "bytes" + "encoding" + "encoding/base64" + "encoding/json" + "fmt" + "math" + "reflect" + "strconv" + "time" + "unsafe" +) + +func (d decoder) decodeNull(b []byte, p unsafe.Pointer) ([]byte, error) { + if hasNullPrefix(b) { + return b[4:], nil + } + return inputError(b, nullType) +} + +func (d decoder) decodeBool(b []byte, p unsafe.Pointer) ([]byte, error) { + switch { + case hasTruePrefix(b): + *(*bool)(p) = true + return b[4:], nil + + case hasFalsePrefix(b): + *(*bool)(p) = false + return b[5:], nil + + case hasNullPrefix(b): + return b[4:], nil + + default: + return inputError(b, boolType) + } +} + +func (d decoder) decodeInt(b []byte, p unsafe.Pointer) ([]byte, error) { + if hasNullPrefix(b) { + return b[4:], nil + } + + v, r, err := parseInt(b, intType) + if err != nil { + return r, err + } + + *(*int)(p) = int(v) + return r, nil +} + +func (d decoder) decodeInt8(b []byte, p unsafe.Pointer) ([]byte, error) { + if hasNullPrefix(b) { + return b[4:], nil + } + + v, r, err := parseInt(b, int8Type) + if err != nil { + return r, err + } + + if v < math.MinInt8 || v > math.MaxInt8 { + return r, unmarshalOverflow(b[:len(b)-len(r)], int8Type) + } + + *(*int8)(p) = int8(v) + return r, nil +} + +func (d decoder) decodeInt16(b []byte, p unsafe.Pointer) ([]byte, error) { + if hasNullPrefix(b) { + return b[4:], nil + } + + v, r, err := parseInt(b, int16Type) + if err != nil { + return r, err + } + + if v < math.MinInt16 || v > math.MaxInt16 { + return r, unmarshalOverflow(b[:len(b)-len(r)], int16Type) + } + + *(*int16)(p) = int16(v) + return r, nil +} + +func (d decoder) decodeInt32(b []byte, p unsafe.Pointer) ([]byte, error) { + if hasNullPrefix(b) { + return b[4:], nil + } + + v, r, err := parseInt(b, int32Type) + if err != nil { + return r, err + } + + if v < math.MinInt32 || v > math.MaxInt32 { + return r, unmarshalOverflow(b[:len(b)-len(r)], int32Type) + } + + *(*int32)(p) = int32(v) + return r, nil +} + +func (d decoder) decodeInt64(b []byte, p unsafe.Pointer) ([]byte, error) { + if hasNullPrefix(b) { + return b[4:], nil + } + + v, r, err := parseInt(b, int64Type) + if err != nil { + return r, err + } + + *(*int64)(p) = v + return r, nil +} + +func (d decoder) decodeUint(b []byte, p unsafe.Pointer) ([]byte, error) { + if hasNullPrefix(b) { + return b[4:], nil + } + + v, r, err := parseUint(b, uintType) + if err != nil { + return r, err + } + + *(*uint)(p) = uint(v) + return r, nil +} + +func (d decoder) decodeUintptr(b []byte, p unsafe.Pointer) ([]byte, error) { + if hasNullPrefix(b) { + return b[4:], nil + } + + v, r, err := parseUint(b, uintptrType) + if err != nil { + return r, err + } + + *(*uintptr)(p) = uintptr(v) + return r, nil +} + +func (d decoder) decodeUint8(b []byte, p unsafe.Pointer) ([]byte, error) { + if hasNullPrefix(b) { + return b[4:], nil + } + + v, r, err := parseUint(b, uint8Type) + if err != nil { + return r, err + } + + if v > math.MaxUint8 { + return r, unmarshalOverflow(b[:len(b)-len(r)], uint8Type) + } + + *(*uint8)(p) = uint8(v) + return r, nil +} + +func (d decoder) decodeUint16(b []byte, p unsafe.Pointer) ([]byte, error) { + if hasNullPrefix(b) { + return b[4:], nil + } + + v, r, err := parseUint(b, uint16Type) + if err != nil { + return r, err + } + + if v > math.MaxUint16 { + return r, unmarshalOverflow(b[:len(b)-len(r)], uint16Type) + } + + *(*uint16)(p) = uint16(v) + return r, nil +} + +func (d decoder) decodeUint32(b []byte, p unsafe.Pointer) ([]byte, error) { + if hasNullPrefix(b) { + return b[4:], nil + } + + v, r, err := parseUint(b, uint32Type) + if err != nil { + return r, err + } + + if v > math.MaxUint32 { + return r, unmarshalOverflow(b[:len(b)-len(r)], uint32Type) + } + + *(*uint32)(p) = uint32(v) + return r, nil +} + +func (d decoder) decodeUint64(b []byte, p unsafe.Pointer) ([]byte, error) { + if hasNullPrefix(b) { + return b[4:], nil + } + + v, r, err := parseUint(b, uint64Type) + if err != nil { + return r, err + } + + *(*uint64)(p) = v + return r, nil +} + +func (d decoder) decodeFloat32(b []byte, p unsafe.Pointer) ([]byte, error) { + if hasNullPrefix(b) { + return b[4:], nil + } + + v, r, err := parseNumber(b) + if err != nil { + return inputError(b, float32Type) + } + + f, err := strconv.ParseFloat(*(*string)(unsafe.Pointer(&v)), 32) + if err != nil { + return inputError(b, float32Type) + } + + *(*float32)(p) = float32(f) + return r, nil +} + +func (d decoder) decodeFloat64(b []byte, p unsafe.Pointer) ([]byte, error) { + if hasNullPrefix(b) { + return b[4:], nil + } + + v, r, err := parseNumber(b) + if err != nil { + return inputError(b, float64Type) + } + + f, err := strconv.ParseFloat(*(*string)(unsafe.Pointer(&v)), 64) + if err != nil { + return inputError(b, float64Type) + } + + *(*float64)(p) = f + return r, nil +} + +func (d decoder) decodeNumber(b []byte, p unsafe.Pointer) ([]byte, error) { + if hasNullPrefix(b) { + return b[4:], nil + } + + v, r, err := parseNumber(b) + if err != nil { + return inputError(b, numberType) + } + + if (d.flags & DontCopyNumber) != 0 { + *(*Number)(p) = *(*Number)(unsafe.Pointer(&v)) + } else { + *(*Number)(p) = Number(v) + } + + return r, nil +} + +func (d decoder) decodeString(b []byte, p unsafe.Pointer) ([]byte, error) { + if hasNullPrefix(b) { + return b[4:], nil + } + + s, r, new, err := parseStringUnquote(b, nil) + if err != nil { + if len(b) == 0 || b[0] != '"' { + return inputError(b, stringType) + } + return r, err + } + + if new || (d.flags&DontCopyString) != 0 { + *(*string)(p) = *(*string)(unsafe.Pointer(&s)) + } else { + *(*string)(p) = string(s) + } + + return r, nil +} + +func (d decoder) decodeFromString(b []byte, p unsafe.Pointer, decode decodeFunc) ([]byte, error) { + if hasNullPrefix(b) { + return decode(d, b, p) + } + + v, b, _, err := parseStringUnquote(b, nil) + if err != nil { + return inputError(v, stringType) + } + + if v, err = decode(d, v, p); err != nil { + return b, err + } + + if v = skipSpaces(v); len(v) != 0 { + return b, syntaxError(v, "unexpected trailing tokens after string value") + } + + return b, nil +} + +func (d decoder) decodeFromStringToInt(b []byte, p unsafe.Pointer, t reflect.Type, decode decodeFunc) ([]byte, error) { + if hasPrefix(b, "null") { + return decode(d, b, p) + } + + if len(b) > 0 && b[0] != '"' { + v, r, err := parseNumber(b) + if err == nil { + // The encoding/json package will return a *json.UnmarshalTypeError if + // the input was a floating point number representation, even tho a + // string is expected here. + isFloat := true + switch { + case bytes.IndexByte(v, '.') >= 0: + case bytes.IndexByte(v, 'e') >= 0: + case bytes.IndexByte(v, 'E') >= 0: + default: + isFloat = false + } + if isFloat { + _, err := strconv.ParseFloat(*(*string)(unsafe.Pointer(&v)), 64) + if err != nil { + return r, unmarshalTypeError(v, t) + } + } + } + return r, fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal unquoted value into int") + } + + if len(b) > 1 && b[0] == '"' && b[1] == '"' { + return b, fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal \"\" into int") + } + + v, b, _, err := parseStringUnquote(b, nil) + if err != nil { + return inputError(v, t) + } + + if hasLeadingZeroes(v) { + // In this context the encoding/json package accepts leading zeroes because + // it is not constrained by the JSON syntax, remove them so the parsing + // functions don't return syntax errors. + u := make([]byte, 0, len(v)) + i := 0 + + if i < len(v) && v[i] == '-' || v[i] == '+' { + u = append(u, v[i]) + i++ + } + + for (i+1) < len(v) && v[i] == '0' && '0' <= v[i+1] && v[i+1] <= '9' { + i++ + } + + v = append(u, v[i:]...) + } + + if r, err := decode(d, v, p); err != nil { + if _, isSyntaxError := err.(*SyntaxError); isSyntaxError { + if hasPrefix(v, "-") { + // The standard library interprets sequences of '-' characters + // as numbers but still returns type errors in this case... + return b, unmarshalTypeError(v, t) + } + return b, fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into int", prefix(v)) + } + // When the input value was a valid number representation we retain the + // error returned by the decoder. + if _, _, err := parseNumber(v); err != nil { + // When the input value valid JSON we mirror the behavior of the + // encoding/json package and return a generic error. + if _, _, err := parseValue(v); err == nil { + return b, fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into int", prefix(v)) + } + } + return b, err + } else if len(r) != 0 { + return r, unmarshalTypeError(v, t) + } + + return b, nil +} + +func (d decoder) decodeBytes(b []byte, p unsafe.Pointer) ([]byte, error) { + if hasNullPrefix(b) { + *(*[]byte)(p) = nil + return b[4:], nil + } + + if len(b) < 2 { + return inputError(b, bytesType) + } + + if b[0] != '"' { + // Go 1.7- behavior: bytes slices may be decoded from array of integers. + if len(b) > 0 && b[0] == '[' { + return d.decodeSlice(b, p, 1, bytesType, decoder.decodeUint8) + } + return inputError(b, bytesType) + } + + // The input string contains escaped sequences, we need to parse it before + // decoding it to match the encoding/json package behvaior. + src, r, _, err := parseStringUnquote(b, nil) + if err != nil { + return inputError(b, bytesType) + } + + dst := make([]byte, base64.StdEncoding.DecodedLen(len(src))) + + n, err := base64.StdEncoding.Decode(dst, src) + if err != nil { + return r, err + } + + *(*[]byte)(p) = dst[:n] + return r, nil +} + +func (d decoder) decodeDuration(b []byte, p unsafe.Pointer) ([]byte, error) { + if hasNullPrefix(b) { + return b[4:], nil + } + + // in order to inter-operate with the stdlib, we must be able to interpret + // durations passed as integer values. there's some discussion about being + // flexible on how durations are formatted, but for the time being, it's + // been punted to go2 at the earliest: https://github.com/golang/go/issues/4712 + if len(b) > 0 && b[0] != '"' { + v, r, err := parseInt(b, durationType) + if err != nil { + return inputError(b, int32Type) + } + + if v < math.MinInt64 || v > math.MaxInt64 { + return r, unmarshalOverflow(b[:len(b)-len(r)], int32Type) + } + + *(*time.Duration)(p) = time.Duration(v) + return r, nil + } + + if len(b) < 2 || b[0] != '"' { + return inputError(b, durationType) + } + + i := bytes.IndexByte(b[1:], '"') + 1 + if i <= 0 { + return inputError(b, durationType) + } + + s := b[1:i] // trim quotes + + v, err := time.ParseDuration(*(*string)(unsafe.Pointer(&s))) + if err != nil { + return inputError(b, durationType) + } + + *(*time.Duration)(p) = v + return b[i+1:], nil +} + +func (d decoder) decodeTime(b []byte, p unsafe.Pointer) ([]byte, error) { + if hasNullPrefix(b) { + return b[4:], nil + } + + if len(b) < 2 || b[0] != '"' { + return inputError(b, timeType) + } + + i := bytes.IndexByte(b[1:], '"') + 1 + if i <= 0 { + return inputError(b, timeType) + } + + s := b[1:i] // trim quotes + + v, err := time.Parse(time.RFC3339Nano, *(*string)(unsafe.Pointer(&s))) + if err != nil { + return inputError(b, timeType) + } + + *(*time.Time)(p) = v + return b[i+1:], nil +} + +func (d decoder) decodeArray(b []byte, p unsafe.Pointer, n int, size uintptr, t reflect.Type, decode decodeFunc) ([]byte, error) { + if hasNullPrefix(b) { + return b[4:], nil + } + + if len(b) < 2 || b[0] != '[' { + return inputError(b, t) + } + b = b[1:] + + var err error + for i := 0; i < n; i++ { + b = skipSpaces(b) + + if i != 0 { + if len(b) == 0 { + return b, syntaxError(b, "unexpected EOF after array element") + } + switch b[0] { + case ',': + b = skipSpaces(b[1:]) + case ']': + return b[1:], nil + default: + return b, syntaxError(b, "expected ',' after array element but found '%c'", b[0]) + } + } + + b, err = decode(d, b, unsafe.Pointer(uintptr(p)+(uintptr(i)*size))) + if err != nil { + if e, ok := err.(*UnmarshalTypeError); ok { + e.Struct = t.String() + e.Struct + e.Field = strconv.Itoa(i) + "." + e.Field + } + return b, err + } + } + + // The encoding/json package ignores extra elements found when decoding into + // array types (which have a fixed size). + for { + b = skipSpaces(b) + + if len(b) == 0 { + return b, syntaxError(b, "missing closing ']' in array value") + } + + switch b[0] { + case ',': + b = skipSpaces(b[1:]) + case ']': + return b[1:], nil + } + + _, b, err = parseValue(b) + if err != nil { + return b, err + } + } +} + +var ( + // This is a placeholder used to consturct non-nil empty slices. + empty struct{} +) + +func (d decoder) decodeSlice(b []byte, p unsafe.Pointer, size uintptr, t reflect.Type, decode decodeFunc) ([]byte, error) { + if hasNullPrefix(b) { + *(*slice)(p) = slice{} + return b[4:], nil + } + + if len(b) < 2 { + return inputError(b, t) + } + + if b[0] != '[' { + // Go 1.7- behavior: fallback to decoding as a []byte if the element + // type is byte; allow conversions from JSON strings even tho the + // underlying type implemented unmarshaler interfaces. + if t.Elem().Kind() == reflect.Uint8 { + return d.decodeBytes(b, p) + } + return inputError(b, t) + } + + input := b + b = b[1:] + + s := (*slice)(p) + s.len = 0 + + var err error + for { + b = skipSpaces(b) + + if len(b) != 0 && b[0] == ']' { + if s.data == nil { + s.data = unsafe.Pointer(&empty) + } + return b[1:], nil + } + + if s.len != 0 { + if len(b) == 0 { + return b, syntaxError(b, "unexpected EOF after array element") + } + if b[0] != ',' { + return b, syntaxError(b, "expected ',' after array element but found '%c'", b[0]) + } + b = skipSpaces(b[1:]) + } + + if s.len == s.cap { + c := s.cap + + if c == 0 { + c = 10 + } else { + c *= 2 + } + + *s = extendSlice(t, s, c) + } + + b, err = decode(d, b, unsafe.Pointer(uintptr(s.data)+(uintptr(s.len)*size))) + if err != nil { + if _, r, err := parseValue(input); err != nil { + return r, err + } else { + b = r + } + if e, ok := err.(*UnmarshalTypeError); ok { + e.Struct = t.String() + e.Struct + e.Field = strconv.Itoa(s.len) + "." + e.Field + } + return b, err + } + + s.len++ + } +} + +func (d decoder) decodeMap(b []byte, p unsafe.Pointer, t, kt, vt reflect.Type, kz, vz reflect.Value, decodeKey, decodeValue decodeFunc) ([]byte, error) { + if hasNullPrefix(b) { + *(*unsafe.Pointer)(p) = nil + return b[4:], nil + } + + if len(b) < 2 || b[0] != '{' { + return inputError(b, t) + } + i := 0 + m := reflect.NewAt(t, p).Elem() + + k := reflect.New(kt).Elem() + v := reflect.New(vt).Elem() + + kptr := (*iface)(unsafe.Pointer(&k)).ptr + vptr := (*iface)(unsafe.Pointer(&v)).ptr + input := b + + if m.IsNil() { + m = reflect.MakeMap(t) + } + + var err error + b = b[1:] + for { + k.Set(kz) + v.Set(vz) + b = skipSpaces(b) + + if len(b) != 0 && b[0] == '}' { + *(*unsafe.Pointer)(p) = unsafe.Pointer(m.Pointer()) + return b[1:], nil + } + + if i != 0 { + if len(b) == 0 { + return b, syntaxError(b, "unexpected end of JONS input after object field value") + } + if b[0] != ',' { + return b, syntaxError(b, "expected ',' after object field value but found '%c'", b[0]) + } + b = skipSpaces(b[1:]) + } + + if hasPrefix(b, "null") { + return b, syntaxError(b, "cannot decode object key string from 'null' value") + } + + if b, err = decodeKey(d, b, kptr); err != nil { + return objectKeyError(b, err) + } + b = skipSpaces(b) + + if len(b) == 0 { + return b, syntaxError(b, "unexpected end of JSON input after object field key") + } + if b[0] != ':' { + return b, syntaxError(b, "expected ':' after object field key but found '%c'", b[0]) + } + b = skipSpaces(b[1:]) + + if b, err = decodeValue(d, b, vptr); err != nil { + if _, r, err := parseValue(input); err != nil { + return r, err + } else { + b = r + } + if e, ok := err.(*UnmarshalTypeError); ok { + e.Struct = "map[" + kt.String() + "]" + vt.String() + "{" + e.Struct + "}" + e.Field = fmt.Sprint(k.Interface()) + "." + e.Field + } + return b, err + } + + m.SetMapIndex(k, v) + i++ + } +} + +func (d decoder) decodeMapStringInterface(b []byte, p unsafe.Pointer) ([]byte, error) { + if hasNullPrefix(b) { + *(*unsafe.Pointer)(p) = nil + return b[4:], nil + } + + if len(b) < 2 || b[0] != '{' { + return inputError(b, mapStringInterfaceType) + } + + i := 0 + m := *(*map[string]interface{})(p) + + if m == nil { + m = make(map[string]interface{}, 64) + } + + var err error + var key string + var val interface{} + var input = b + + b = b[1:] + for { + key = "" + val = nil + + b = skipSpaces(b) + + if len(b) != 0 && b[0] == '}' { + *(*unsafe.Pointer)(p) = *(*unsafe.Pointer)(unsafe.Pointer(&m)) + return b[1:], nil + } + + if i != 0 { + if len(b) == 0 { + return b, syntaxError(b, "unexpected end of JSON input after object field value") + } + if b[0] != ',' { + return b, syntaxError(b, "expected ',' after object field value but found '%c'", b[0]) + } + b = skipSpaces(b[1:]) + } + + if hasPrefix(b, "null") { + return b, syntaxError(b, "cannot decode object key string from 'null' value") + } + + b, err = d.decodeString(b, unsafe.Pointer(&key)) + if err != nil { + return objectKeyError(b, err) + } + b = skipSpaces(b) + + if len(b) == 0 { + return b, syntaxError(b, "unexpected end of JSON input after object field key") + } + if b[0] != ':' { + return b, syntaxError(b, "expected ':' after object field key but found '%c'", b[0]) + } + b = skipSpaces(b[1:]) + + b, err = d.decodeInterface(b, unsafe.Pointer(&val)) + if err != nil { + if _, r, err := parseValue(input); err != nil { + return r, err + } else { + b = r + } + if e, ok := err.(*UnmarshalTypeError); ok { + e.Struct = mapStringInterfaceType.String() + e.Struct + e.Field = key + "." + e.Field + } + return b, err + } + + m[key] = val + i++ + } +} + +func (d decoder) decodeMapStringRawMessage(b []byte, p unsafe.Pointer) ([]byte, error) { + if hasNullPrefix(b) { + *(*unsafe.Pointer)(p) = nil + return b[4:], nil + } + + if len(b) < 2 || b[0] != '{' { + return inputError(b, mapStringRawMessageType) + } + + i := 0 + m := *(*map[string]RawMessage)(p) + + if m == nil { + m = make(map[string]RawMessage, 64) + } + + var err error + var key string + var val RawMessage + var input = b + + b = b[1:] + for { + key = "" + val = nil + + b = skipSpaces(b) + + if len(b) != 0 && b[0] == '}' { + *(*unsafe.Pointer)(p) = *(*unsafe.Pointer)(unsafe.Pointer(&m)) + return b[1:], nil + } + + if i != 0 { + if len(b) == 0 { + return b, syntaxError(b, "unexpected end of JSON input after object field value") + } + if b[0] != ',' { + return b, syntaxError(b, "expected ',' after object field value but found '%c'", b[0]) + } + b = skipSpaces(b[1:]) + } + + if hasPrefix(b, "null") { + return b, syntaxError(b, "cannot decode object key string from 'null' value") + } + + b, err = d.decodeString(b, unsafe.Pointer(&key)) + if err != nil { + return objectKeyError(b, err) + } + b = skipSpaces(b) + + if len(b) == 0 { + return b, syntaxError(b, "unexpected end of JSON input after object field key") + } + if b[0] != ':' { + return b, syntaxError(b, "expected ':' after object field key but found '%c'", b[0]) + } + b = skipSpaces(b[1:]) + + b, err = d.decodeRawMessage(b, unsafe.Pointer(&val)) + if err != nil { + if _, r, err := parseValue(input); err != nil { + return r, err + } else { + b = r + } + if e, ok := err.(*UnmarshalTypeError); ok { + e.Struct = mapStringRawMessageType.String() + e.Struct + e.Field = key + "." + e.Field + } + return b, err + } + + m[key] = val + i++ + } +} + +func (d decoder) decodeStruct(b []byte, p unsafe.Pointer, st *structType) ([]byte, error) { + if hasNullPrefix(b) { + return b[4:], nil + } + + if len(b) < 2 || b[0] != '{' { + return inputError(b, st.typ) + } + + var err error + var k []byte + var i int + + // memory buffer used to convert short field names to lowercase + var buf [64]byte + var key []byte + var input = b + + b = b[1:] + for { + b = skipSpaces(b) + + if len(b) != 0 && b[0] == '}' { + return b[1:], nil + } + + if i != 0 { + if len(b) == 0 { + return b, syntaxError(b, "unexpected end of JSON input after object field value") + } + if b[0] != ',' { + return b, syntaxError(b, "expected ',' after object field value but found '%c'", b[0]) + } + b = skipSpaces(b[1:]) + } + i++ + + if hasPrefix(b, "null") { + return b, syntaxError(b, "cannot decode object key string from 'null' value") + } + + k, b, _, err = parseStringUnquote(b, nil) + if err != nil { + return objectKeyError(b, err) + } + b = skipSpaces(b) + + if len(b) == 0 { + return b, syntaxError(b, "unexpected end of JSON input after object field key") + } + if b[0] != ':' { + return b, syntaxError(b, "expected ':' after object field key but found '%c'", b[0]) + } + b = skipSpaces(b[1:]) + + f := st.fieldsIndex[string(k)] + + if f == nil && (d.flags&DontMatchCaseInsensitiveStructFields) == 0 { + key = appendToLower(buf[:0], k) + f = st.ficaseIndex[string(key)] + } + + if f == nil { + if (d.flags & DisallowUnknownFields) != 0 { + return b, fmt.Errorf("json: unknown field %q", k) + } + if _, b, err = parseValue(b); err != nil { + return b, err + } + continue + } + + if b, err = f.codec.decode(d, b, unsafe.Pointer(uintptr(p)+f.offset)); err != nil { + if _, r, err := parseValue(input); err != nil { + return r, err + } else { + b = r + } + if e, ok := err.(*UnmarshalTypeError); ok { + e.Struct = st.typ.String() + e.Struct + e.Field = string(k) + "." + e.Field + } + return b, err + } + } +} + +func (d decoder) decodeEmbeddedStructPointer(b []byte, p unsafe.Pointer, t reflect.Type, unexported bool, offset uintptr, decode decodeFunc) ([]byte, error) { + v := *(*unsafe.Pointer)(p) + + if v == nil { + if unexported { + return nil, fmt.Errorf("json: cannot set embedded pointer to unexported struct: %s", t) + } + v = unsafe.Pointer(reflect.New(t).Pointer()) + *(*unsafe.Pointer)(p) = v + } + + return decode(d, b, unsafe.Pointer(uintptr(v)+offset)) +} + +func (d decoder) decodePointer(b []byte, p unsafe.Pointer, t reflect.Type, decode decodeFunc) ([]byte, error) { + if hasNullPrefix(b) { + pp := *(*unsafe.Pointer)(p) + if pp != nil && t.Kind() == reflect.Ptr { + return decode(d, b, pp) + } + *(*unsafe.Pointer)(p) = nil + return b[4:], nil + } + + v := *(*unsafe.Pointer)(p) + if v == nil { + v = unsafe.Pointer(reflect.New(t).Pointer()) + *(*unsafe.Pointer)(p) = v + } + + return decode(d, b, v) +} + +func (d decoder) decodeInterface(b []byte, p unsafe.Pointer) ([]byte, error) { + val := *(*interface{})(p) + *(*interface{})(p) = nil + + if t := reflect.TypeOf(val); t != nil && t.Kind() == reflect.Ptr { + if v := reflect.ValueOf(val); v.IsNil() || t.Elem().Kind() != reflect.Ptr { + // If the destination is nil the only value that is OK to decode is + // `null`, and the encoding/json package always nils the destination + // interface value in this case. + if hasNullPrefix(b) { + *(*interface{})(p) = nil + return b[4:], nil + } + } + + b, err := Parse(b, val, d.flags) + if err == nil { + *(*interface{})(p) = val + } + return b, err + } + + v, b, err := parseValue(b) + if err != nil { + return b, err + } + + switch v[0] { + case '{': + m := make(map[string]interface{}) + v, err = d.decodeMapStringInterface(v, unsafe.Pointer(&m)) + val = m + + case '[': + a := make([]interface{}, 0, 10) + v, err = d.decodeSlice(v, unsafe.Pointer(&a), unsafe.Sizeof(a[0]), sliceInterfaceType, decoder.decodeInterface) + val = a + + case '"': + s := "" + v, err = d.decodeString(v, unsafe.Pointer(&s)) + val = s + + case 'n': + v, err = d.decodeNull(v, nil) + val = nil + + case 't', 'f': + x := false + v, err = d.decodeBool(v, unsafe.Pointer(&x)) + val = x + + case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + if (d.flags & UseNumber) != 0 { + n := Number("") + v, err = d.decodeNumber(v, unsafe.Pointer(&n)) + val = n + } else { + f := 0.0 + v, err = d.decodeFloat64(v, unsafe.Pointer(&f)) + val = f + } + + default: + return b, syntaxError(v, "expected token but found '%c'", v[0]) + } + + if err != nil { + return b, err + } + + if v = skipSpaces(v); len(v) != 0 { + return b, syntaxError(v, "unexpected trailing trailing tokens after json value") + } + + *(*interface{})(p) = val + return b, nil +} + +func (d decoder) decodeMaybeEmptyInterface(b []byte, p unsafe.Pointer, t reflect.Type) ([]byte, error) { + if hasNullPrefix(b) { + *(*interface{})(p) = nil + return b[4:], nil + } + + if x := reflect.NewAt(t, p).Elem(); !x.IsNil() { + if e := x.Elem(); e.Kind() == reflect.Ptr { + return Parse(b, e.Interface(), d.flags) + } + } else if t.NumMethod() == 0 { // empty interface + return Parse(b, (*interface{})(p), d.flags) + } + + return d.decodeUnmarshalTypeError(b, p, t) +} + +func (d decoder) decodeUnmarshalTypeError(b []byte, p unsafe.Pointer, t reflect.Type) ([]byte, error) { + v, b, err := parseValue(b) + if err != nil { + return b, err + } + return b, &UnmarshalTypeError{ + Value: string(v), + Type: t, + } +} + +func (d decoder) decodeRawMessage(b []byte, p unsafe.Pointer) ([]byte, error) { + v, r, err := parseValue(b) + if err != nil { + return inputError(b, rawMessageType) + } + + if (d.flags & DontCopyRawMessage) == 0 { + v = append(make([]byte, 0, len(v)), v...) + } + + *(*RawMessage)(p) = json.RawMessage(v) + return r, err +} + +func (d decoder) decodeJSONUnmarshaler(b []byte, p unsafe.Pointer, t reflect.Type, pointer bool) ([]byte, error) { + v, b, err := parseValue(b) + if err != nil { + return b, err + } + + u := reflect.NewAt(t, p) + if !pointer { + u = u.Elem() + t = t.Elem() + } + if u.IsNil() { + u.Set(reflect.New(t)) + } + + return b, u.Interface().(Unmarshaler).UnmarshalJSON(v) +} + +func (d decoder) decodeTextUnmarshaler(b []byte, p unsafe.Pointer, t reflect.Type, pointer bool) ([]byte, error) { + var value string + + v, b, err := parseValue(b) + if err != nil { + return b, err + } + if len(v) == 0 { + return inputError(v, t) + } + + switch v[0] { + case 'n': + _, _, err := parseNull(v) + return b, err + case '"': + s, _, _, err := parseStringUnquote(v, nil) + if err != nil { + return b, err + } + u := reflect.NewAt(t, p) + if !pointer { + u = u.Elem() + t = t.Elem() + } + if u.IsNil() { + u.Set(reflect.New(t)) + } + return b, u.Interface().(encoding.TextUnmarshaler).UnmarshalText(s) + case '{': + value = "object" + case '[': + value = "array" + case 't': + value = "true" + case 'f': + value = "false" + case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + value = "number" + } + + return b, &UnmarshalTypeError{Value: value, Type: reflect.PtrTo(t)} +} diff --git a/utils/encoding/json/encode.go b/utils/encoding/json/encode.go new file mode 100644 index 00000000..e6e1e29c --- /dev/null +++ b/utils/encoding/json/encode.go @@ -0,0 +1,744 @@ +package json + +import ( + "encoding" + "encoding/base64" + "math" + "reflect" + "sort" + "strconv" + "sync" + "time" + "unicode/utf8" + "unsafe" +) + +const hex = "0123456789abcdef" + +func (e encoder) encodeNull(b []byte, p unsafe.Pointer) ([]byte, error) { + return append(b, "null"...), nil +} + +func (e encoder) encodeBool(b []byte, p unsafe.Pointer) ([]byte, error) { + if *(*bool)(p) { + return append(b, "true"...), nil + } + return append(b, "false"...), nil +} + +func (e encoder) encodeInt(b []byte, p unsafe.Pointer) ([]byte, error) { + return strconv.AppendInt(b, int64(*(*int)(p)), 10), nil +} + +func (e encoder) encodeInt8(b []byte, p unsafe.Pointer) ([]byte, error) { + return strconv.AppendInt(b, int64(*(*int8)(p)), 10), nil +} + +func (e encoder) encodeInt16(b []byte, p unsafe.Pointer) ([]byte, error) { + return strconv.AppendInt(b, int64(*(*int16)(p)), 10), nil +} + +func (e encoder) encodeInt32(b []byte, p unsafe.Pointer) ([]byte, error) { + return strconv.AppendInt(b, int64(*(*int32)(p)), 10), nil +} + +func (e encoder) encodeInt64(b []byte, p unsafe.Pointer) ([]byte, error) { + return strconv.AppendInt(b, *(*int64)(p), 10), nil +} + +func (e encoder) encodeUint(b []byte, p unsafe.Pointer) ([]byte, error) { + return strconv.AppendUint(b, uint64(*(*uint)(p)), 10), nil +} + +func (e encoder) encodeUintptr(b []byte, p unsafe.Pointer) ([]byte, error) { + return strconv.AppendUint(b, uint64(*(*uintptr)(p)), 10), nil +} + +func (e encoder) encodeUint8(b []byte, p unsafe.Pointer) ([]byte, error) { + return strconv.AppendUint(b, uint64(*(*uint8)(p)), 10), nil +} + +func (e encoder) encodeUint16(b []byte, p unsafe.Pointer) ([]byte, error) { + return strconv.AppendUint(b, uint64(*(*uint16)(p)), 10), nil +} + +func (e encoder) encodeUint32(b []byte, p unsafe.Pointer) ([]byte, error) { + return strconv.AppendUint(b, uint64(*(*uint32)(p)), 10), nil +} + +func (e encoder) encodeUint64(b []byte, p unsafe.Pointer) ([]byte, error) { + return strconv.AppendUint(b, *(*uint64)(p), 10), nil +} + +func (e encoder) encodeFloat32(b []byte, p unsafe.Pointer) ([]byte, error) { + return e.encodeFloat(b, float64(*(*float32)(p)), 32) +} + +func (e encoder) encodeFloat64(b []byte, p unsafe.Pointer) ([]byte, error) { + return e.encodeFloat(b, *(*float64)(p), 64) +} + +func (e encoder) encodeFloat(b []byte, f float64, bits int) ([]byte, error) { + switch { + case math.IsNaN(f): + return b, &UnsupportedValueError{Value: reflect.ValueOf(f), Str: "NaN"} + case math.IsInf(f, 0): + return b, &UnsupportedValueError{Value: reflect.ValueOf(f), Str: "inf"} + } + + // Convert as if by ES6 number to string conversion. + // This matches most other JSON generators. + // See golang.org/issue/6384 and golang.org/issue/14135. + // Like fmt %g, but the exponent cutoffs are different + // and exponents themselves are not padded to two digits. + abs := math.Abs(f) + fmt := byte('f') + // Note: Must use float32 comparisons for underlying float32 value to get precise cutoffs right. + if abs != 0 { + if bits == 64 && (abs < 1e-6 || abs >= 1e21) || bits == 32 && (float32(abs) < 1e-6 || float32(abs) >= 1e21) { + fmt = 'e' + } + } + + b = strconv.AppendFloat(b, f, fmt, -1, int(bits)) + + if fmt == 'e' { + // clean up e-09 to e-9 + n := len(b) + if n >= 4 && b[n-4] == 'e' && b[n-3] == '-' && b[n-2] == '0' { + b[n-2] = b[n-1] + b = b[:n-1] + } + } + + return b, nil +} + +func (e encoder) encodeNumber(b []byte, p unsafe.Pointer) ([]byte, error) { + n := *(*Number)(p) + if n == "" { + n = "0" + } + + _, _, err := parseNumber(stringToBytes(string(n))) + if err != nil { + return b, err + } + + return append(b, n...), nil +} + +func (e encoder) encodeString(b []byte, p unsafe.Pointer) ([]byte, error) { + s := *(*string)(p) + i := 0 + j := 0 + escapeHTML := (e.flags & EscapeHTML) != 0 + + b = append(b, '"') + + for j < len(s) { + c := s[j] + + if c >= 0x20 && c <= 0x7f && c != '\\' && c != '"' && (!escapeHTML || (c != '<' && c != '>' && c != '&')) { + // fast path: most of the time, printable ascii characters are used + j++ + continue + } + + switch c { + case '\\', '"': + b = append(b, s[i:j]...) + b = append(b, '\\', c) + i = j + 1 + j = j + 1 + continue + + case '\n': + b = append(b, s[i:j]...) + b = append(b, '\\', 'n') + i = j + 1 + j = j + 1 + continue + + case '\r': + b = append(b, s[i:j]...) + b = append(b, '\\', 'r') + i = j + 1 + j = j + 1 + continue + + case '\t': + b = append(b, s[i:j]...) + b = append(b, '\\', 't') + i = j + 1 + j = j + 1 + continue + + case '<', '>', '&': + b = append(b, s[i:j]...) + b = append(b, `\u00`...) + b = append(b, hex[c>>4], hex[c&0xF]) + i = j + 1 + j = j + 1 + continue + } + + // This encodes bytes < 0x20 except for \t, \n and \r. + if c < 0x20 { + b = append(b, s[i:j]...) + b = append(b, `\u00`...) + b = append(b, hex[c>>4], hex[c&0xF]) + i = j + 1 + j = j + 1 + continue + } + + r, size := utf8.DecodeRuneInString(s[j:]) + + if r == utf8.RuneError && size == 1 { + b = append(b, s[i:j]...) + b = append(b, `\ufffd`...) + i = j + size + j = j + size + continue + } + + switch r { + case '\u2028', '\u2029': + // U+2028 is LINE SEPARATOR. + // U+2029 is PARAGRAPH SEPARATOR. + // They are both technically valid characters in JSON strings, + // but don't work in JSONP, which has to be evaluated as JavaScript, + // and can lead to security holes there. It is valid JSON to + // escape them, so we do so unconditionally. + // See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion. + b = append(b, s[i:j]...) + b = append(b, `\u202`...) + b = append(b, hex[r&0xF]) + i = j + size + j = j + size + continue + } + + j += size + } + + b = append(b, s[i:]...) + b = append(b, '"') + return b, nil +} + +func (e encoder) encodeToString(b []byte, p unsafe.Pointer, encode encodeFunc) ([]byte, error) { + i := len(b) + + b, err := encode(e, b, p) + if err != nil { + return b, err + } + + j := len(b) + s := b[i:] + + if b, err = e.encodeString(b, unsafe.Pointer(&s)); err != nil { + return b, err + } + + n := copy(b[i:], b[j:]) + return b[:i+n], nil +} + +func (e encoder) encodeBytes(b []byte, p unsafe.Pointer) ([]byte, error) { + v := *(*[]byte)(p) + if v == nil { + return append(b, "null"...), nil + } + + n := base64.StdEncoding.EncodedLen(len(v)) + 2 + + if avail := cap(b) - len(b); avail < n { + newB := make([]byte, cap(b)+(n-avail)) + copy(newB, b) + b = newB[:len(b)] + } + + i := len(b) + j := len(b) + n + + b = b[:j] + b[i] = '"' + base64.StdEncoding.Encode(b[i+1:j-1], v) + b[j-1] = '"' + return b, nil +} + +func (e encoder) encodeDuration(b []byte, p unsafe.Pointer) ([]byte, error) { + b = append(b, '"') + b = appendDuration(b, *(*time.Duration)(p)) + b = append(b, '"') + return b, nil +} + +func (e encoder) encodeTime(b []byte, p unsafe.Pointer) ([]byte, error) { + t := *(*time.Time)(p) + b = append(b, '"') + b = t.AppendFormat(b, time.RFC3339Nano) + b = append(b, '"') + return b, nil +} + +func (e encoder) encodeArray(b []byte, p unsafe.Pointer, n int, size uintptr, t reflect.Type, encode encodeFunc) ([]byte, error) { + var start = len(b) + var err error + b = append(b, '[') + + for i := 0; i < n; i++ { + if i != 0 { + b = append(b, ',') + } + if b, err = encode(e, b, unsafe.Pointer(uintptr(p)+(uintptr(i)*size))); err != nil { + return b[:start], err + } + } + + b = append(b, ']') + return b, nil +} + +func (e encoder) encodeSlice(b []byte, p unsafe.Pointer, size uintptr, t reflect.Type, encode encodeFunc) ([]byte, error) { + s := (*slice)(p) + + if s.data == nil && s.len == 0 && s.cap == 0 { + return append(b, "null"...), nil + } + + return e.encodeArray(b, s.data, s.len, size, t, encode) +} + +func (e encoder) encodeMap(b []byte, p unsafe.Pointer, t reflect.Type, encodeKey, encodeValue encodeFunc, sortKeys sortFunc) ([]byte, error) { + m := reflect.NewAt(t, p).Elem() + if m.IsNil() { + return append(b, "null"...), nil + } + + keys := m.MapKeys() + if sortKeys != nil && (e.flags&SortMapKeys) != 0 { + sortKeys(keys) + } + + var start = len(b) + var err error + b = append(b, '{') + + for i, k := range keys { + v := m.MapIndex(k) + + if i != 0 { + b = append(b, ',') + } + + if b, err = encodeKey(e, b, (*iface)(unsafe.Pointer(&k)).ptr); err != nil { + return b[:start], err + } + + b = append(b, ':') + + if b, err = encodeValue(e, b, (*iface)(unsafe.Pointer(&v)).ptr); err != nil { + return b[:start], err + } + } + + b = append(b, '}') + return b, nil +} + +type element struct { + key string + val interface{} + raw RawMessage +} + +type mapslice struct { + elements []element +} + +func (m *mapslice) Len() int { return len(m.elements) } +func (m *mapslice) Less(i, j int) bool { return m.elements[i].key < m.elements[j].key } +func (m *mapslice) Swap(i, j int) { m.elements[i], m.elements[j] = m.elements[j], m.elements[i] } + +var mapslicePool = sync.Pool{ + New: func() interface{} { return new(mapslice) }, +} + +func (e encoder) encodeMapStringInterface(b []byte, p unsafe.Pointer) ([]byte, error) { + m := *(*map[string]interface{})(p) + if m == nil { + return append(b, "null"...), nil + } + + if (e.flags & SortMapKeys) == 0 { + // Optimized code path when the program does not need the map keys to be + // sorted. + b = append(b, '{') + + if len(m) != 0 { + var err error + var i = 0 + + for k, v := range m { + if i != 0 { + b = append(b, ',') + } + + b, _ = e.encodeString(b, unsafe.Pointer(&k)) + b = append(b, ':') + + b, err = Append(b, v, e.flags) + if err != nil { + return b, err + } + + i++ + } + } + + b = append(b, '}') + return b, nil + } + + s := mapslicePool.Get().(*mapslice) + if cap(s.elements) < len(m) { + s.elements = make([]element, 0, align(10, uintptr(len(m)))) + } + for key, val := range m { + s.elements = append(s.elements, element{key: key, val: val}) + } + sort.Sort(s) + + var start = len(b) + var err error + b = append(b, '{') + + for i, elem := range s.elements { + if i != 0 { + b = append(b, ',') + } + + b, _ = e.encodeString(b, unsafe.Pointer(&elem.key)) + b = append(b, ':') + + b, err = Append(b, elem.val, e.flags) + if err != nil { + break + } + } + + for i := range s.elements { + s.elements[i] = element{} + } + + s.elements = s.elements[:0] + mapslicePool.Put(s) + + if err != nil { + return b[:start], err + } + + b = append(b, '}') + return b, nil +} + +func (e encoder) encodeMapStringRawMessage(b []byte, p unsafe.Pointer) ([]byte, error) { + m := *(*map[string]RawMessage)(p) + if m == nil { + return append(b, "null"...), nil + } + + if (e.flags & SortMapKeys) == 0 { + // Optimized code path when the program does not need the map keys to be + // sorted. + b = append(b, '{') + + if len(m) != 0 { + var err error + var i = 0 + + for k, v := range m { + if i != 0 { + b = append(b, ',') + } + + b, _ = e.encodeString(b, unsafe.Pointer(&k)) + b = append(b, ':') + + b, err = e.encodeRawMessage(b, unsafe.Pointer(&v)) + if err != nil { + break + } + + i++ + } + } + + b = append(b, '}') + return b, nil + } + + s := mapslicePool.Get().(*mapslice) + if cap(s.elements) < len(m) { + s.elements = make([]element, 0, align(10, uintptr(len(m)))) + } + for key, raw := range m { + s.elements = append(s.elements, element{key: key, raw: raw}) + } + sort.Sort(s) + + var start = len(b) + var err error + b = append(b, '{') + + for i, elem := range s.elements { + if i != 0 { + b = append(b, ',') + } + + b, _ = e.encodeString(b, unsafe.Pointer(&elem.key)) + b = append(b, ':') + + b, err = e.encodeRawMessage(b, unsafe.Pointer(&elem.raw)) + if err != nil { + break + } + } + + for i := range s.elements { + s.elements[i] = element{} + } + + s.elements = s.elements[:0] + mapslicePool.Put(s) + + if err != nil { + return b[:start], err + } + + b = append(b, '}') + return b, nil +} + +func (e encoder) encodeStruct(b []byte, p unsafe.Pointer, st *structType) ([]byte, error) { + var start = len(b) + var err error + var k string + var n int + b = append(b, '{') + + for i := range st.fields { + f := &st.fields[i] + v := unsafe.Pointer(uintptr(p) + f.offset) + + if f.omitempty && f.empty(v) { + continue + } + + if (e.flags & EscapeHTML) != 0 { + k = f.html + } else { + k = f.json + } + + lengthBeforeKey := len(b) + + if n != 0 { + b = append(b, ',') + } + + b = append(b, k...) + b = append(b, ':') + + if b, err = f.codec.encode(e, b, v); err != nil { + if err == (rollback{}) { + b = b[:lengthBeforeKey] + continue + } + return b[:start], err + } + + n++ + } + + b = append(b, '}') + return b, nil +} + +type rollback struct{} + +func (rollback) Error() string { return "rollback" } + +func (e encoder) encodeEmbeddedStructPointer(b []byte, p unsafe.Pointer, t reflect.Type, unexported bool, offset uintptr, encode encodeFunc) ([]byte, error) { + p = *(*unsafe.Pointer)(p) + if p == nil { + return b, rollback{} + } + return encode(e, b, unsafe.Pointer(uintptr(p)+offset)) +} + +func (e encoder) encodePointer(b []byte, p unsafe.Pointer, t reflect.Type, encode encodeFunc) ([]byte, error) { + if p = *(*unsafe.Pointer)(p); p != nil { + return encode(e, b, p) + } + return e.encodeNull(b, nil) +} + +func (e encoder) encodeInterface(b []byte, p unsafe.Pointer) ([]byte, error) { + return Append(b, *(*interface{})(p), e.flags) +} + +func (e encoder) encodeMaybeEmptyInterface(b []byte, p unsafe.Pointer, t reflect.Type) ([]byte, error) { + return Append(b, reflect.NewAt(t, p).Elem().Interface(), e.flags) +} + +func (e encoder) encodeUnsupportedTypeError(b []byte, p unsafe.Pointer, t reflect.Type) ([]byte, error) { + return b, &UnsupportedTypeError{Type: t} +} + +func (e encoder) encodeRawMessage(b []byte, p unsafe.Pointer) ([]byte, error) { + v := *(*RawMessage)(p) + + if v == nil { + return append(b, "null"...), nil + } + + var s []byte + + if (e.flags & TrustRawMessage) != 0 { + s = v + } else { + var err error + s, _, err = parseValue(v) + if err != nil { + return b, &UnsupportedValueError{Value: reflect.ValueOf(v), Str: err.Error()} + } + } + + if (e.flags & EscapeHTML) != 0 { + return appendCompactEscapeHTML(b, s), nil + } + + return append(b, s...), nil +} + +func (e encoder) encodeJSONMarshaler(b []byte, p unsafe.Pointer, t reflect.Type, pointer bool) ([]byte, error) { + v := reflect.NewAt(t, p) + + if !pointer { + v = v.Elem() + } + + switch v.Kind() { + case reflect.Ptr, reflect.Interface: + if v.IsNil() { + return append(b, "null"...), nil + } + } + + j, err := v.Interface().(Marshaler).MarshalJSON() + if err != nil { + return b, err + } + + s, _, err := parseValue(j) + if err != nil { + return b, &MarshalerError{Type: t, Err: err} + } + + if (e.flags & EscapeHTML) != 0 { + return appendCompactEscapeHTML(b, s), nil + } + + return append(b, s...), nil +} + +func (e encoder) encodeTextMarshaler(b []byte, p unsafe.Pointer, t reflect.Type, pointer bool) ([]byte, error) { + v := reflect.NewAt(t, p) + + if !pointer { + v = v.Elem() + } + + switch v.Kind() { + case reflect.Ptr, reflect.Interface: + if v.IsNil() { + return append(b, `null`...), nil + } + } + + s, err := v.Interface().(encoding.TextMarshaler).MarshalText() + if err != nil { + return b, err + } + + return e.encodeString(b, unsafe.Pointer(&s)) +} + +func appendCompactEscapeHTML(dst []byte, src []byte) []byte { + start := 0 + escape := false + inString := false + + for i, c := range src { + if !inString { + switch c { + case '"': // enter string + inString = true + case ' ', '\n', '\r', '\t': // skip space + if start < i { + dst = append(dst, src[start:i]...) + } + start = i + 1 + } + continue + } + + if escape { + escape = false + continue + } + + if c == '\\' { + escape = true + continue + } + + if c == '"' { + inString = false + continue + } + + if c == '<' || c == '>' || c == '&' { + if start < i { + dst = append(dst, src[start:i]...) + } + dst = append(dst, `\u00`...) + dst = append(dst, hex[c>>4], hex[c&0xF]) + start = i + 1 + continue + } + + // Convert U+2028 and U+2029 (E2 80 A8 and E2 80 A9). + if c == 0xE2 && i+2 < len(src) && src[i+1] == 0x80 && src[i+2]&^1 == 0xA8 { + if start < i { + dst = append(dst, src[start:i]...) + } + dst = append(dst, `\u202`...) + dst = append(dst, hex[src[i+2]&0xF]) + start = i + 3 + continue + } + } + + if start < len(src) { + dst = append(dst, src[start:]...) + } + + return dst +} diff --git a/utils/encoding/json/golang_bench_test.go b/utils/encoding/json/golang_bench_test.go new file mode 100644 index 00000000..07cc378c --- /dev/null +++ b/utils/encoding/json/golang_bench_test.go @@ -0,0 +1,374 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Large data benchmark. +// The JSON data is a summary of agl's changes in the +// go, webkit, and chromium open source projects. +// We benchmark converting between the JSON form +// and in-memory data structures. + +package json + +import ( + "bytes" + "compress/gzip" + "fmt" + "io/ioutil" + "os" + "reflect" + "runtime" + "strings" + "sync" + "testing" +) + +type codeResponse struct { + Tree *codeNode `json:"tree"` + Username string `json:"username"` +} + +type codeNode struct { + Name string `json:"name"` + Kids []*codeNode `json:"kids"` + CLWeight float64 `json:"cl_weight"` + Touches int `json:"touches"` + MinT int64 `json:"min_t"` + MaxT int64 `json:"max_t"` + MeanT int64 `json:"mean_t"` +} + +var codeJSON []byte +var codeStruct codeResponse + +func codeInit() { + f, err := os.Open("testdata/code.json.gz") + if err != nil { + panic(err) + } + defer f.Close() + gz, err := gzip.NewReader(f) + if err != nil { + panic(err) + } + data, err := ioutil.ReadAll(gz) + if err != nil { + panic(err) + } + + codeJSON = data + + if err := Unmarshal(codeJSON, &codeStruct); err != nil { + panic("unmarshal code.json: " + err.Error()) + } + + if data, err = Marshal(&codeStruct); err != nil { + panic("marshal code.json: " + err.Error()) + } + + if !bytes.Equal(data, codeJSON) { + println("different lengths", len(data), len(codeJSON)) + for i := 0; i < len(data) && i < len(codeJSON); i++ { + if data[i] != codeJSON[i] { + println("re-marshal: changed at byte", i) + println("orig: ", string(codeJSON[i-10:i+10])) + println("new: ", string(data[i-10:i+10])) + break + } + } + panic("re-marshal code.json: different result") + } +} + +func BenchmarkCodeEncoder(b *testing.B) { + b.ReportAllocs() + if codeJSON == nil { + b.StopTimer() + codeInit() + b.StartTimer() + } + b.RunParallel(func(pb *testing.PB) { + enc := NewEncoder(ioutil.Discard) + for pb.Next() { + if err := enc.Encode(&codeStruct); err != nil { + b.Fatal("Encode:", err) + } + } + }) + b.SetBytes(int64(len(codeJSON))) +} + +func BenchmarkCodeMarshal(b *testing.B) { + b.ReportAllocs() + if codeJSON == nil { + b.StopTimer() + codeInit() + b.StartTimer() + } + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + if _, err := Marshal(&codeStruct); err != nil { + b.Fatal("Marshal:", err) + } + } + }) + b.SetBytes(int64(len(codeJSON))) +} + +func benchMarshalBytes(n int) func(*testing.B) { + sample := []byte("hello world") + // Use a struct pointer, to avoid an allocation when passing it as an + // interface parameter to Marshal. + v := &struct { + Bytes []byte + }{ + bytes.Repeat(sample, (n/len(sample))+1)[:n], + } + return func(b *testing.B) { + for i := 0; i < b.N; i++ { + if _, err := Marshal(v); err != nil { + b.Fatal("Marshal:", err) + } + } + } +} + +func BenchmarkMarshalBytes(b *testing.B) { + b.ReportAllocs() + // 32 fits within encodeState.scratch. + b.Run("32", benchMarshalBytes(32)) + // 256 doesn't fit in encodeState.scratch, but is small enough to + // allocate and avoid the slower base64.NewEncoder. + b.Run("256", benchMarshalBytes(256)) + // 4096 is large enough that we want to avoid allocating for it. + b.Run("4096", benchMarshalBytes(4096)) +} + +func BenchmarkCodeDecoder(b *testing.B) { + b.ReportAllocs() + if codeJSON == nil { + b.StopTimer() + codeInit() + b.StartTimer() + } + b.RunParallel(func(pb *testing.PB) { + var buf bytes.Buffer + dec := NewDecoder(&buf) + var r codeResponse + for pb.Next() { + buf.Write(codeJSON) + // hide EOF + buf.WriteByte('\n') + buf.WriteByte('\n') + buf.WriteByte('\n') + if err := dec.Decode(&r); err != nil { + b.Fatal("Decode:", err) + } + } + }) + b.SetBytes(int64(len(codeJSON))) +} + +func BenchmarkUnicodeDecoder(b *testing.B) { + b.ReportAllocs() + j := []byte(`"\uD83D\uDE01"`) + b.SetBytes(int64(len(j))) + r := bytes.NewReader(j) + dec := NewDecoder(r) + var out string + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := dec.Decode(&out); err != nil { + b.Fatal("Decode:", err) + } + r.Seek(0, 0) + } +} + +func BenchmarkDecoderStream(b *testing.B) { + b.ReportAllocs() + b.StopTimer() + var buf bytes.Buffer + dec := NewDecoder(&buf) + buf.WriteString(`"` + strings.Repeat("x", 1000000) + `"` + "\n\n\n") + var x interface{} + if err := dec.Decode(&x); err != nil { + b.Fatal("Decode:", err) + } + ones := strings.Repeat(" 1\n", 300000) + "\n\n\n" + b.StartTimer() + for i := 0; i < b.N; i++ { + if i%300000 == 0 { + buf.WriteString(ones) + } + x = nil + if err := dec.Decode(&x); err != nil || x != 1.0 { + b.Fatalf("Decode: %v after %d", err, i) + } + } +} + +func BenchmarkCodeUnmarshal(b *testing.B) { + b.ReportAllocs() + if codeJSON == nil { + b.StopTimer() + codeInit() + b.StartTimer() + } + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var r codeResponse + if err := Unmarshal(codeJSON, &r); err != nil { + b.Fatal("Unmarshal:", err) + } + } + }) + b.SetBytes(int64(len(codeJSON))) +} + +func BenchmarkCodeUnmarshalReuse(b *testing.B) { + b.ReportAllocs() + if codeJSON == nil { + b.StopTimer() + codeInit() + b.StartTimer() + } + b.RunParallel(func(pb *testing.PB) { + var r codeResponse + for pb.Next() { + if err := Unmarshal(codeJSON, &r); err != nil { + b.Fatal("Unmarshal:", err) + } + } + }) + b.SetBytes(int64(len(codeJSON))) +} + +func BenchmarkUnmarshalString(b *testing.B) { + b.ReportAllocs() + data := []byte(`"hello, world"`) + b.RunParallel(func(pb *testing.PB) { + var s string + for pb.Next() { + if err := Unmarshal(data, &s); err != nil { + b.Fatal("Unmarshal:", err) + } + } + }) +} + +func BenchmarkUnmarshalFloat64(b *testing.B) { + b.ReportAllocs() + data := []byte(`3.14`) + b.RunParallel(func(pb *testing.PB) { + var f float64 + for pb.Next() { + if err := Unmarshal(data, &f); err != nil { + b.Fatal("Unmarshal:", err) + } + } + }) +} + +func BenchmarkUnmarshalInt64(b *testing.B) { + b.ReportAllocs() + data := []byte(`3`) + b.RunParallel(func(pb *testing.PB) { + var x int64 + for pb.Next() { + if err := Unmarshal(data, &x); err != nil { + b.Fatal("Unmarshal:", err) + } + } + }) +} + +func BenchmarkIssue10335(b *testing.B) { + b.ReportAllocs() + j := []byte(`{"a":{ }}`) + b.RunParallel(func(pb *testing.PB) { + var s struct{} + for pb.Next() { + if err := Unmarshal(j, &s); err != nil { + b.Fatal(err) + } + } + }) +} + +func BenchmarkUnmapped(b *testing.B) { + b.ReportAllocs() + j := []byte(`{"s": "hello", "y": 2, "o": {"x": 0}, "a": [1, 99, {"x": 1}]}`) + b.RunParallel(func(pb *testing.PB) { + var s struct{} + for pb.Next() { + if err := Unmarshal(j, &s); err != nil { + b.Fatal(err) + } + } + }) +} + +func BenchmarkTypeFieldsCache(b *testing.B) { + b.ReportAllocs() + var maxTypes int = 1e6 + if testenv.Builder() != "" { + maxTypes = 1e3 // restrict cache sizes on builders + } + + // Dynamically generate many new types. + types := make([]reflect.Type, maxTypes) + fs := []reflect.StructField{{ + Type: reflect.TypeOf(""), + Index: []int{0}, + }} + for i := range types { + fs[0].Name = fmt.Sprintf("TypeFieldsCache%d", i) + types[i] = reflect.StructOf(fs) + } + + // clearClear clears the cache. Other JSON operations, must not be running. + clearCache := func() { + fieldCache = sync.Map{} + } + + // MissTypes tests the performance of repeated cache misses. + // This measures the time to rebuild a cache of size nt. + for nt := 1; nt <= maxTypes; nt *= 10 { + ts := types[:nt] + b.Run(fmt.Sprintf("MissTypes%d", nt), func(b *testing.B) { + nc := runtime.GOMAXPROCS(0) + for i := 0; i < b.N; i++ { + clearCache() + var wg sync.WaitGroup + for j := 0; j < nc; j++ { + wg.Add(1) + go func(j int) { + for _, t := range ts[(j*len(ts))/nc : ((j+1)*len(ts))/nc] { + cachedTypeFields(t) + } + wg.Done() + }(j) + } + wg.Wait() + } + }) + } + + // HitTypes tests the performance of repeated cache hits. + // This measures the average time of each cache lookup. + for nt := 1; nt <= maxTypes; nt *= 10 { + // Pre-warm a cache of size nt. + clearCache() + for _, t := range types[:nt] { + cachedTypeFields(t) + } + b.Run(fmt.Sprintf("HitTypes%d", nt), func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + cachedTypeFields(types[0]) + } + }) + }) + } +} diff --git a/utils/encoding/json/golang_decode_test.go b/utils/encoding/json/golang_decode_test.go new file mode 100644 index 00000000..0cc7af42 --- /dev/null +++ b/utils/encoding/json/golang_decode_test.go @@ -0,0 +1,2346 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package json + +import ( + "bytes" + "encoding" + "errors" + "fmt" + "image" + "math" + "math/big" + "net" + "reflect" + "strconv" + "strings" + "testing" + "time" +) + +type T struct { + X string + Y int + Z int `json:"-"` +} + +type U struct { + Alphabet string `json:"alpha"` +} + +type V struct { + F1 interface{} + F2 int32 + F3 Number + F4 *VOuter +} + +type VOuter struct { + V V +} + +type W struct { + S SS +} + +type P struct { + PP PP +} + +type PP struct { + T T + Ts []T +} + +type SS string + +func (*SS) UnmarshalJSON(data []byte) error { + return &UnmarshalTypeError{Value: "number", Type: reflect.TypeOf(SS(""))} +} + +// ifaceNumAsFloat64/ifaceNumAsNumber are used to test unmarshaling with and +// without UseNumber +var ifaceNumAsFloat64 = map[string]interface{}{ + "k1": float64(1), + "k2": "s", + "k3": []interface{}{float64(1), float64(2.0), float64(3e-3)}, + "k4": map[string]interface{}{"kk1": "s", "kk2": float64(2)}, +} + +var ifaceNumAsNumber = map[string]interface{}{ + "k1": Number("1"), + "k2": "s", + "k3": []interface{}{Number("1"), Number("2.0"), Number("3e-3")}, + "k4": map[string]interface{}{"kk1": "s", "kk2": Number("2")}, +} + +type tx struct { + x int +} + +type u8 uint8 + +// A type that can unmarshal itself. + +type unmarshaler struct { + T bool +} + +func (u *unmarshaler) UnmarshalJSON(b []byte) error { + *u = unmarshaler{true} // All we need to see that UnmarshalJSON is called. + return nil +} + +type ustruct struct { + M unmarshaler +} + +type unmarshalerText struct { + A, B string +} + +// needed for re-marshaling tests +func (u unmarshalerText) MarshalText() ([]byte, error) { + return []byte(u.A + ":" + u.B), nil +} + +func (u *unmarshalerText) UnmarshalText(b []byte) error { + pos := bytes.IndexByte(b, ':') + if pos == -1 { + return errors.New("missing separator") + } + u.A, u.B = string(b[:pos]), string(b[pos+1:]) + return nil +} + +var _ encoding.TextUnmarshaler = (*unmarshalerText)(nil) + +type ustructText struct { + M unmarshalerText +} + +// u8marshal is an integer type that can marshal/unmarshal itself. +type u8marshal uint8 + +func (u8 u8marshal) MarshalText() ([]byte, error) { + return []byte(fmt.Sprintf("u%d", u8)), nil +} + +var errMissingU8Prefix = errors.New("missing 'u' prefix") + +func (u8 *u8marshal) UnmarshalText(b []byte) error { + if !bytes.HasPrefix(b, []byte{'u'}) { + return errMissingU8Prefix + } + n, err := strconv.Atoi(string(b[1:])) + if err != nil { + return err + } + *u8 = u8marshal(n) + return nil +} + +var _ encoding.TextUnmarshaler = (*u8marshal)(nil) + +var ( + um0, um1 unmarshaler // target2 of unmarshaling + ump = &um1 + umtrue = unmarshaler{true} + umslice = []unmarshaler{{true}} + umslicep = new([]unmarshaler) + umstruct = ustruct{unmarshaler{true}} + + um0T, um1T unmarshalerText // target2 of unmarshaling + umpType = &um1T + umtrueXY = unmarshalerText{"x", "y"} + umsliceXY = []unmarshalerText{{"x", "y"}} + umslicepType = new([]unmarshalerText) + umstructType = new(ustructText) + umstructXY = ustructText{unmarshalerText{"x", "y"}} + + ummapType = map[unmarshalerText]bool{} + ummapXY = map[unmarshalerText]bool{{"x", "y"}: true} +) + +// Test data structures for anonymous fields. + +type Point struct { + Z int +} + +type Top struct { + Level0 int + Embed0 + *Embed0a + *Embed0b `json:"e,omitempty"` // treated as named + Embed0c `json:"-"` // ignored + Loop + Embed0p // has Point with X, Y, used + Embed0q // has Point with Z, used + embed // contains exported field +} + +type Embed0 struct { + Level1a int // overridden by Embed0a's Level1a with json tag + Level1b int // used because Embed0a's Level1b is renamed + Level1c int // used because Embed0a's Level1c is ignored + Level1d int // annihilated by Embed0a's Level1d + Level1e int `json:"x"` // annihilated by Embed0a.Level1e +} + +type Embed0a struct { + Level1a int `json:"Level1a,omitempty"` + Level1b int `json:"LEVEL1B,omitempty"` + Level1c int `json:"-"` + Level1d int // annihilated by Embed0's Level1d + Level1f int `json:"x"` // annihilated by Embed0's Level1e +} + +type Embed0b Embed0 + +type Embed0c Embed0 + +type Embed0p struct { + image.Point +} + +type Embed0q struct { + Point +} + +type embed struct { + Q int +} + +type Loop struct { + Loop1 int `json:",omitempty"` + Loop2 int `json:",omitempty"` + *Loop +} + +// From reflect test: +// The X in S6 and S7 annihilate, but they also block the X in S8.S9. +type S5 struct { + S6 + S7 + S8 +} + +type S6 struct { + X int +} + +type S7 S6 + +type S8 struct { + S9 +} + +type S9 struct { + X int + Y int +} + +// From reflect test: +// The X in S11.S6 and S12.S6 annihilate, but they also block the X in S13.S8.S9. +type S10 struct { + S11 + S12 + S13 +} + +type S11 struct { + S6 +} + +type S12 struct { + S6 +} + +type S13 struct { + S8 +} + +type Ambig struct { + // Given "hello", the first match should win. + First int `json:"HELLO"` + Second int `json:"Hello"` +} + +type XYZ struct { + X interface{} + Y interface{} + Z interface{} +} + +type unexportedWithMethods struct{} + +func (unexportedWithMethods) F() {} + +func sliceAddr(x []int) *[]int { return &x } +func mapAddr(x map[string]int) *map[string]int { return &x } + +type byteWithMarshalJSON byte + +func (b byteWithMarshalJSON) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf(`"Z%.2x"`, byte(b))), nil +} + +func (b *byteWithMarshalJSON) UnmarshalJSON(data []byte) error { + if len(data) != 5 || data[0] != '"' || data[1] != 'Z' || data[4] != '"' { + return fmt.Errorf("bad quoted string") + } + i, err := strconv.ParseInt(string(data[2:4]), 16, 8) + if err != nil { + return fmt.Errorf("bad hex") + } + *b = byteWithMarshalJSON(i) + return nil +} + +type byteWithPtrMarshalJSON byte + +func (b *byteWithPtrMarshalJSON) MarshalJSON() ([]byte, error) { + return byteWithMarshalJSON(*b).MarshalJSON() +} + +func (b *byteWithPtrMarshalJSON) UnmarshalJSON(data []byte) error { + return (*byteWithMarshalJSON)(b).UnmarshalJSON(data) +} + +type byteWithMarshalText byte + +func (b byteWithMarshalText) MarshalText() ([]byte, error) { + return []byte(fmt.Sprintf(`Z%.2x`, byte(b))), nil +} + +func (b *byteWithMarshalText) UnmarshalText(data []byte) error { + if len(data) != 3 || data[0] != 'Z' { + return fmt.Errorf("bad quoted string") + } + i, err := strconv.ParseInt(string(data[1:3]), 16, 8) + if err != nil { + return fmt.Errorf("bad hex") + } + *b = byteWithMarshalText(i) + return nil +} + +type byteWithPtrMarshalText byte + +func (b *byteWithPtrMarshalText) MarshalText() ([]byte, error) { + return byteWithMarshalText(*b).MarshalText() +} + +func (b *byteWithPtrMarshalText) UnmarshalText(data []byte) error { + return (*byteWithMarshalText)(b).UnmarshalText(data) +} + +type intWithMarshalJSON int + +func (b intWithMarshalJSON) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf(`"Z%.2x"`, int(b))), nil +} + +func (b *intWithMarshalJSON) UnmarshalJSON(data []byte) error { + if len(data) != 5 || data[0] != '"' || data[1] != 'Z' || data[4] != '"' { + return fmt.Errorf("bad quoted string") + } + i, err := strconv.ParseInt(string(data[2:4]), 16, 8) + if err != nil { + return fmt.Errorf("bad hex") + } + *b = intWithMarshalJSON(i) + return nil +} + +type intWithPtrMarshalJSON int + +func (b *intWithPtrMarshalJSON) MarshalJSON() ([]byte, error) { + return intWithMarshalJSON(*b).MarshalJSON() +} + +func (b *intWithPtrMarshalJSON) UnmarshalJSON(data []byte) error { + return (*intWithMarshalJSON)(b).UnmarshalJSON(data) +} + +type intWithMarshalText int + +func (b intWithMarshalText) MarshalText() ([]byte, error) { + return []byte(fmt.Sprintf(`Z%.2x`, int(b))), nil +} + +func (b *intWithMarshalText) UnmarshalText(data []byte) error { + if len(data) != 3 || data[0] != 'Z' { + return fmt.Errorf("bad quoted string") + } + i, err := strconv.ParseInt(string(data[1:3]), 16, 8) + if err != nil { + return fmt.Errorf("bad hex") + } + *b = intWithMarshalText(i) + return nil +} + +type intWithPtrMarshalText int + +func (b *intWithPtrMarshalText) MarshalText() ([]byte, error) { + return intWithMarshalText(*b).MarshalText() +} + +func (b *intWithPtrMarshalText) UnmarshalText(data []byte) error { + return (*intWithMarshalText)(b).UnmarshalText(data) +} + +type mapStringToStringData struct { + Data map[string]string `json:"data"` +} + +type unmarshalTest struct { + in string + ptr interface{} + out interface{} + err error + useNumber bool + golden bool + disallowUnknownFields bool +} + +type B struct { + B bool `json:",string"` +} + +var unmarshalTests = []unmarshalTest{ + // basic types + {in: `true`, ptr: new(bool), out: true}, + {in: `1`, ptr: new(int), out: 1}, + {in: `1.2`, ptr: new(float64), out: 1.2}, + {in: `-5`, ptr: new(int16), out: int16(-5)}, + {in: `2`, ptr: new(Number), out: Number("2"), useNumber: true}, + {in: `2`, ptr: new(Number), out: Number("2")}, + {in: `2`, ptr: new(interface{}), out: float64(2.0)}, + {in: `2`, ptr: new(interface{}), out: Number("2"), useNumber: true}, + {in: `"a\u1234"`, ptr: new(string), out: "a\u1234"}, + {in: `"http:\/\/"`, ptr: new(string), out: "http://"}, + {in: `"g-clef: \uD834\uDD1E"`, ptr: new(string), out: "g-clef: \U0001D11E"}, + //TODO + //{in: `"invalid: \uD834x\uDD1E"`, ptr: new(string), out: "invalid: \uFFFDx\uFFFD"}, + {in: "null", ptr: new(interface{}), out: nil}, + {in: `{"X": [1,2,3], "Y": 4}`, ptr: new(T), out: T{Y: 4}, err: &UnmarshalTypeError{"array", reflect.TypeOf(""), 7, "T", "X"}}, + {in: `{"X": 23}`, ptr: new(T), out: T{}, err: &UnmarshalTypeError{"number", reflect.TypeOf(""), 8, "T", "X"}}, {in: `{"x": 1}`, ptr: new(tx), out: tx{}}, + {in: `{"x": 1}`, ptr: new(tx), out: tx{}}, + {in: `{"x": 1}`, ptr: new(tx), err: fmt.Errorf("json: unknown field \"x\""), disallowUnknownFields: true}, + {in: `{"S": 23}`, ptr: new(W), out: W{}, err: &UnmarshalTypeError{"number", reflect.TypeOf(SS("")), 0, "W", "S"}}, + {in: `{"F1":1,"F2":2,"F3":3}`, ptr: new(V), out: V{F1: float64(1), F2: int32(2), F3: Number("3")}}, + {in: `{"F1":1,"F2":2,"F3":3}`, ptr: new(V), out: V{F1: Number("1"), F2: int32(2), F3: Number("3")}, useNumber: true}, + {in: `{"k1":1,"k2":"s","k3":[1,2.0,3e-3],"k4":{"kk1":"s","kk2":2}}`, ptr: new(interface{}), out: ifaceNumAsFloat64}, + {in: `{"k1":1,"k2":"s","k3":[1,2.0,3e-3],"k4":{"kk1":"s","kk2":2}}`, ptr: new(interface{}), out: ifaceNumAsNumber, useNumber: true}, + + // raw values with whitespace + {in: "\n true ", ptr: new(bool), out: true}, + {in: "\t 1 ", ptr: new(int), out: 1}, + {in: "\r 1.2 ", ptr: new(float64), out: 1.2}, + {in: "\t -5 \n", ptr: new(int16), out: int16(-5)}, + {in: "\t \"a\\u1234\" \n", ptr: new(string), out: "a\u1234"}, + + // Z has a "-" tag. + {in: `{"Y": 1, "Z": 2}`, ptr: new(T), out: T{Y: 1}}, + {in: `{"Y": 1, "Z": 2}`, ptr: new(T), err: fmt.Errorf("json: unknown field \"Z\""), disallowUnknownFields: true}, + + {in: `{"alpha": "abc", "alphabet": "xyz"}`, ptr: new(U), out: U{Alphabet: "abc"}}, + {in: `{"alpha": "abc", "alphabet": "xyz"}`, ptr: new(U), err: fmt.Errorf("json: unknown field \"alphabet\""), disallowUnknownFields: true}, + {in: `{"alpha": "abc"}`, ptr: new(U), out: U{Alphabet: "abc"}}, + {in: `{"alphabet": "xyz"}`, ptr: new(U), out: U{}}, + {in: `{"alphabet": "xyz"}`, ptr: new(U), err: fmt.Errorf("json: unknown field \"alphabet\""), disallowUnknownFields: true}, + + // syntax errors + {in: `{"X": "foo", "Y"}`, err: &testSyntaxError{"invalid character '}' after object key", 17}}, + {in: `[1, 2, 3+]`, err: &testSyntaxError{"invalid character '+' after array element", 9}}, + {in: `{"X":12x}`, err: &testSyntaxError{"invalid character 'x' after object key:value pair", 8}, useNumber: true}, + {in: `[2, 3`, err: &testSyntaxError{msg: "unexpected end of JSON input", Offset: 5}}, + + // raw value errors + {in: "\x01 42", err: &testSyntaxError{"invalid character '\\x01' looking for beginning of value", 1}}, + {in: " 42 \x01", err: &testSyntaxError{"invalid character '\\x01' after top-level value", 5}}, + {in: "\x01 true", err: &testSyntaxError{"invalid character '\\x01' looking for beginning of value", 1}}, + {in: " false \x01", err: &testSyntaxError{"invalid character '\\x01' after top-level value", 8}}, + {in: "\x01 1.2", err: &testSyntaxError{"invalid character '\\x01' looking for beginning of value", 1}}, + {in: " 3.4 \x01", err: &testSyntaxError{"invalid character '\\x01' after top-level value", 6}}, + {in: "\x01 \"string\"", err: &testSyntaxError{"invalid character '\\x01' looking for beginning of value", 1}}, + {in: " \"string\" \x01", err: &testSyntaxError{"invalid character '\\x01' after top-level value", 11}}, + + // array tests + {in: `[1, 2, 3]`, ptr: new([3]int), out: [3]int{1, 2, 3}}, + {in: `[1, 2, 3]`, ptr: new([1]int), out: [1]int{1}}, + {in: `[1, 2, 3]`, ptr: new([5]int), out: [5]int{1, 2, 3, 0, 0}}, + {in: `[1, 2, 3]`, ptr: new(MustNotUnmarshalJSON), err: errors.New("MustNotUnmarshalJSON was used")}, + + // empty array to interface test + {in: `[]`, ptr: new([]interface{}), out: []interface{}{}}, + {in: `null`, ptr: new([]interface{}), out: []interface{}(nil)}, + {in: `{"T":[]}`, ptr: new(map[string]interface{}), out: map[string]interface{}{"T": []interface{}{}}}, + {in: `{"T":null}`, ptr: new(map[string]interface{}), out: map[string]interface{}{"T": interface{}(nil)}}, + + // composite tests + {in: allValueIndent, ptr: new(All), out: allValue}, + {in: allValueCompact, ptr: new(All), out: allValue}, + {in: allValueIndent, ptr: new(*All), out: &allValue}, + {in: allValueCompact, ptr: new(*All), out: &allValue}, + {in: pallValueIndent, ptr: new(All), out: pallValue}, + {in: pallValueCompact, ptr: new(All), out: pallValue}, + {in: pallValueIndent, ptr: new(*All), out: &pallValue}, + {in: pallValueCompact, ptr: new(*All), out: &pallValue}, + + // unmarshal interface test + {in: `{"T":false}`, ptr: &um0, out: umtrue}, // use "false" so test will fail if custom unmarshaler is not called + {in: `{"T":false}`, ptr: &ump, out: &umtrue}, + {in: `[{"T":false}]`, ptr: &umslice, out: umslice}, + {in: `[{"T":false}]`, ptr: &umslicep, out: &umslice}, + {in: `{"M":{"T":"x:y"}}`, ptr: &umstruct, out: umstruct}, + + // UnmarshalText interface test + {in: `"x:y"`, ptr: &um0T, out: umtrueXY}, + {in: `"x:y"`, ptr: &umpType, out: &umtrueXY}, + {in: `["x:y"]`, ptr: &umsliceXY, out: umsliceXY}, + {in: `["x:y"]`, ptr: &umslicepType, out: &umsliceXY}, + {in: `{"M":"x:y"}`, ptr: umstructType, out: umstructXY}, + + // integer-keyed map test + { + in: `{"-1":"a","0":"b","1":"c"}`, + ptr: new(map[int]string), + out: map[int]string{-1: "a", 0: "b", 1: "c"}, + }, + { + in: `{"0":"a","10":"c","9":"b"}`, + ptr: new(map[u8]string), + out: map[u8]string{0: "a", 9: "b", 10: "c"}, + }, + { + in: `{"-9223372036854775808":"min","9223372036854775807":"max"}`, + ptr: new(map[int64]string), + out: map[int64]string{math.MinInt64: "min", math.MaxInt64: "max"}, + }, + { + in: `{"18446744073709551615":"max"}`, + ptr: new(map[uint64]string), + out: map[uint64]string{math.MaxUint64: "max"}, + }, + { + in: `{"0":false,"10":true}`, + ptr: new(map[uintptr]bool), + out: map[uintptr]bool{0: false, 10: true}, + }, + + // Check that MarshalText and UnmarshalText take precedence + // over default integer handling in map keys. + { + in: `{"u2":4}`, + ptr: new(map[u8marshal]int), + out: map[u8marshal]int{2: 4}, + }, + { + in: `{"2":4}`, + ptr: new(map[u8marshal]int), + err: errMissingU8Prefix, + }, + + // integer-keyed map errors + { + in: `{"abc":"abc"}`, + ptr: new(map[int]string), + err: &UnmarshalTypeError{Value: "number abc", Type: reflect.TypeOf(0), Offset: 2}, + }, + { + in: `{"256":"abc"}`, + ptr: new(map[uint8]string), + err: &UnmarshalTypeError{Value: "number 256", Type: reflect.TypeOf(uint8(0)), Offset: 2}, + }, + { + in: `{"128":"abc"}`, + ptr: new(map[int8]string), + err: &UnmarshalTypeError{Value: "number 128", Type: reflect.TypeOf(int8(0)), Offset: 2}, + }, + { + in: `{"-1":"abc"}`, + ptr: new(map[uint8]string), + err: &UnmarshalTypeError{Value: "number -1", Type: reflect.TypeOf(uint8(0)), Offset: 2}, + }, + { + in: `{"F":{"a":2,"3":4}}`, + ptr: new(map[string]map[int]int), + err: &UnmarshalTypeError{Value: "number a", Type: reflect.TypeOf(int(0)), Offset: 7}, + }, + { + in: `{"F":{"a":2,"3":4}}`, + ptr: new(map[string]map[uint]int), + err: &UnmarshalTypeError{Value: "number a", Type: reflect.TypeOf(uint(0)), Offset: 7}, + }, + + // Map keys can be encoding.TextUnmarshalers. + {in: `{"x:y":true}`, ptr: &ummapType, out: ummapXY}, + // If multiple values for the same key exists, only the most recent value is used. + {in: `{"x:y":false,"x:y":true}`, ptr: &ummapType, out: ummapXY}, + + // Overwriting of data. + // This is different from package xml, but it's what we've always done. + // Now documented and tested. + {in: `[2]`, ptr: sliceAddr([]int{1}), out: []int{2}}, + {in: `{"key": 2}`, ptr: mapAddr(map[string]int{"old": 0, "key": 1}), out: map[string]int{"key": 2}}, + + { + in: `{ + "Level0": 1, + "Level1b": 2, + "Level1c": 3, + "x": 4, + "Level1a": 5, + "LEVEL1B": 6, + "e": { + "Level1a": 8, + "Level1b": 9, + "Level1c": 10, + "Level1d": 11, + "x": 12 + }, + "Loop1": 13, + "Loop2": 14, + "X": 15, + "Y": 16, + "Z": 17, + "Q": 18 + }`, + ptr: new(Top), + out: Top{ + Level0: 1, + Embed0: Embed0{ + Level1b: 2, + Level1c: 3, + }, + Embed0a: &Embed0a{ + Level1a: 5, + Level1b: 6, + }, + Embed0b: &Embed0b{ + Level1a: 8, + Level1b: 9, + Level1c: 10, + Level1d: 11, + Level1e: 12, + }, + Loop: Loop{ + Loop1: 13, + Loop2: 14, + }, + Embed0p: Embed0p{ + Point: image.Point{X: 15, Y: 16}, + }, + Embed0q: Embed0q{ + Point: Point{Z: 17}, + }, + embed: embed{ + Q: 18, + }, + }, + }, + { + in: `{"hello": 1}`, + ptr: new(Ambig), + out: Ambig{First: 1}, + }, + { + in: `{"X": 1,"Y":2}`, + ptr: new(S5), + out: S5{S8: S8{S9: S9{Y: 2}}}, + }, + { + in: `{"X": 1,"Y":2}`, + ptr: new(S5), + err: fmt.Errorf("json: unknown field \"X\""), + disallowUnknownFields: true, + }, + { + in: `{"X": 1,"Y":2}`, + ptr: new(S10), + out: S10{S13: S13{S8: S8{S9: S9{Y: 2}}}}, + }, + { + in: `{"X": 1,"Y":2}`, + ptr: new(S10), + err: fmt.Errorf("json: unknown field \"X\""), + disallowUnknownFields: true, + }, + + // invalid UTF-8 is coerced to valid UTF-8. + { + in: "\"hello\xffworld\"", + ptr: new(string), + out: "hello\ufffdworld", + }, + { + in: "\"hello\xc2\xc2world\"", + ptr: new(string), + out: "hello\ufffd\ufffdworld", + }, + { + in: "\"hello\xc2\xffworld\"", + ptr: new(string), + out: "hello\ufffd\ufffdworld", + }, + { + in: "\"hello\\ud800world\"", + ptr: new(string), + out: "hello\ufffdworld", + }, + { + in: "\"hello\\ud800\\ud800world\"", + ptr: new(string), + out: "hello\ufffd\ufffdworld", + }, + { + in: "\"hello\\ud800\\ud800world\"", + ptr: new(string), + out: "hello\ufffd\ufffdworld", + }, + { + in: "\"hello\xed\xa0\x80\xed\xb0\x80world\"", + ptr: new(string), + out: "hello\ufffd\ufffd\ufffd\ufffd\ufffd\ufffdworld", + }, + + // Used to be issue 8305, but time.Time implements encoding.TextUnmarshaler so this works now. + { + in: `{"2009-11-10T23:00:00Z": "hello world"}`, + ptr: &map[time.Time]string{}, + out: map[time.Time]string{time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC): "hello world"}, + }, + + // issue 8305 + { + in: `{"2009-11-10T23:00:00Z": "hello world"}`, + ptr: &map[Point]string{}, + err: &UnmarshalTypeError{Value: "object", Type: reflect.TypeOf(map[Point]string{}), Offset: 1}, + }, + { + in: `{"asdf": "hello world"}`, + ptr: &map[unmarshaler]string{}, + err: &UnmarshalTypeError{Value: "object", Type: reflect.TypeOf(map[unmarshaler]string{}), Offset: 1}, + }, + + // related to issue 13783. + // Go 1.7 changed marshaling a slice of typed byte to use the methods on the byte type, + // similar to marshaling a slice of typed int. + // These tests check that, assuming the byte type also has valid decoding methods, + // either the old base64 string encoding or the new per-element encoding can be + // successfully unmarshaled. The custom unmarshalers were accessible in earlier + // versions of Go, even though the custom marshaler was not. + { + in: `"AQID"`, + ptr: new([]byteWithMarshalJSON), + out: []byteWithMarshalJSON{1, 2, 3}, + }, + { + in: `["Z01","Z02","Z03"]`, + ptr: new([]byteWithMarshalJSON), + out: []byteWithMarshalJSON{1, 2, 3}, + golden: true, + }, + { + in: `"AQID"`, + ptr: new([]byteWithMarshalText), + out: []byteWithMarshalText{1, 2, 3}, + }, + { + in: `["Z01","Z02","Z03"]`, + ptr: new([]byteWithMarshalText), + out: []byteWithMarshalText{1, 2, 3}, + golden: true, + }, + { + in: `"AQID"`, + ptr: new([]byteWithPtrMarshalJSON), + out: []byteWithPtrMarshalJSON{1, 2, 3}, + }, + { + in: `["Z01","Z02","Z03"]`, + ptr: new([]byteWithPtrMarshalJSON), + out: []byteWithPtrMarshalJSON{1, 2, 3}, + golden: true, + }, + { + in: `"AQID"`, + ptr: new([]byteWithPtrMarshalText), + out: []byteWithPtrMarshalText{1, 2, 3}, + }, + { + in: `["Z01","Z02","Z03"]`, + ptr: new([]byteWithPtrMarshalText), + out: []byteWithPtrMarshalText{1, 2, 3}, + golden: true, + }, + + // ints work with the marshaler but not the base64 []byte case + { + in: `["Z01","Z02","Z03"]`, + ptr: new([]intWithMarshalJSON), + out: []intWithMarshalJSON{1, 2, 3}, + golden: true, + }, + { + in: `["Z01","Z02","Z03"]`, + ptr: new([]intWithMarshalText), + out: []intWithMarshalText{1, 2, 3}, + golden: true, + }, + { + in: `["Z01","Z02","Z03"]`, + ptr: new([]intWithPtrMarshalJSON), + out: []intWithPtrMarshalJSON{1, 2, 3}, + golden: true, + }, + { + in: `["Z01","Z02","Z03"]`, + ptr: new([]intWithPtrMarshalText), + out: []intWithPtrMarshalText{1, 2, 3}, + golden: true, + }, + + {in: `0.000001`, ptr: new(float64), out: 0.000001, golden: true}, + {in: `1e-7`, ptr: new(float64), out: 1e-7, golden: true}, + {in: `100000000000000000000`, ptr: new(float64), out: 100000000000000000000.0, golden: true}, + {in: `1e+21`, ptr: new(float64), out: 1e21, golden: true}, + {in: `-0.000001`, ptr: new(float64), out: -0.000001, golden: true}, + {in: `-1e-7`, ptr: new(float64), out: -1e-7, golden: true}, + {in: `-100000000000000000000`, ptr: new(float64), out: -100000000000000000000.0, golden: true}, + {in: `-1e+21`, ptr: new(float64), out: -1e21, golden: true}, + {in: `999999999999999900000`, ptr: new(float64), out: 999999999999999900000.0, golden: true}, + {in: `9007199254740992`, ptr: new(float64), out: 9007199254740992.0, golden: true}, + {in: `9007199254740993`, ptr: new(float64), out: 9007199254740992.0, golden: false}, + + { + in: `{"V": {"F2": "hello"}}`, + ptr: new(VOuter), + err: &UnmarshalTypeError{ + Value: "string", + Struct: "V", + Field: "V.F2", + Type: reflect.TypeOf(int32(0)), + Offset: 20, + }, + }, + { + in: `{"V": {"F4": {}, "F2": "hello"}}`, + ptr: new(VOuter), + err: &UnmarshalTypeError{ + Value: "string", + Struct: "V", + Field: "V.F2", + Type: reflect.TypeOf(int32(0)), + Offset: 30, + }, + }, + + // issue 15146. + // invalid inputs in wrongStringTests below. + {in: `{"B":"true"}`, ptr: new(B), out: B{true}, golden: true}, + {in: `{"B":"false"}`, ptr: new(B), out: B{false}, golden: true}, + {in: `{"B": "maybe"}`, ptr: new(B), err: errors.New(`json: invalid use of ,string struct tag, trying to unmarshal "maybe" into bool`)}, + {in: `{"B": "tru"}`, ptr: new(B), err: errors.New(`json: invalid use of ,string struct tag, trying to unmarshal "tru" into bool`)}, + {in: `{"B": "False"}`, ptr: new(B), err: errors.New(`json: invalid use of ,string struct tag, trying to unmarshal "False" into bool`)}, + {in: `{"B": "null"}`, ptr: new(B), out: B{false}}, + {in: `{"B": "nul"}`, ptr: new(B), err: errors.New(`json: invalid use of ,string struct tag, trying to unmarshal "nul" into bool`)}, + {in: `{"B": [2, 3]}`, ptr: new(B), err: errors.New(`json: invalid use of ,string struct tag, trying to unmarshal unquoted value into bool`)}, + + // additional tests for disallowUnknownFields + { + in: `{ + "Level0": 1, + "Level1b": 2, + "Level1c": 3, + "x": 4, + "Level1a": 5, + "LEVEL1B": 6, + "e": { + "Level1a": 8, + "Level1b": 9, + "Level1c": 10, + "Level1d": 11, + "x": 12 + }, + "Loop1": 13, + "Loop2": 14, + "X": 15, + "Y": 16, + "Z": 17, + "Q": 18, + "extra": true + }`, + ptr: new(Top), + err: fmt.Errorf("json: unknown field \"extra\""), + disallowUnknownFields: true, + }, + { + in: `{ + "Level0": 1, + "Level1b": 2, + "Level1c": 3, + "x": 4, + "Level1a": 5, + "LEVEL1B": 6, + "e": { + "Level1a": 8, + "Level1b": 9, + "Level1c": 10, + "Level1d": 11, + "x": 12, + "extra": null + }, + "Loop1": 13, + "Loop2": 14, + "X": 15, + "Y": 16, + "Z": 17, + "Q": 18 + }`, + ptr: new(Top), + err: fmt.Errorf("json: unknown field \"extra\""), + disallowUnknownFields: true, + }, + // issue 26444 + // UnmarshalTypeError without field & struct values + { + in: `{"data":{"test1": "bob", "test2": 123}}`, + ptr: new(mapStringToStringData), + err: &UnmarshalTypeError{Value: "number", Type: reflect.TypeOf(""), Offset: 37, Struct: "mapStringToStringData", Field: "data"}, + }, + { + in: `{"data":{"test1": 123, "test2": "bob"}}`, + ptr: new(mapStringToStringData), + err: &UnmarshalTypeError{Value: "number", Type: reflect.TypeOf(""), Offset: 21, Struct: "mapStringToStringData", Field: "data"}, + }, + + // trying to decode JSON arrays or objects via TextUnmarshaler + { + in: `[1, 2, 3]`, + ptr: new(MustNotUnmarshalText), + err: &UnmarshalTypeError{Value: "array", Type: reflect.TypeOf(&MustNotUnmarshalText{}), Offset: 1}, + }, + { + in: `{"foo": "bar"}`, + ptr: new(MustNotUnmarshalText), + err: &UnmarshalTypeError{Value: "object", Type: reflect.TypeOf(&MustNotUnmarshalText{}), Offset: 1}, + }, + // #22369 + { + in: `{"PP": {"T": {"Y": "bad-type"}}}`, + ptr: new(P), + err: &UnmarshalTypeError{ + Value: "string", + Struct: "T", + Field: "PP.T.Y", + Type: reflect.TypeOf(int(0)), + Offset: 29, + }, + }, + { + in: `{"Ts": [{"Y": 1}, {"Y": 2}, {"Y": "bad-type"}]}`, + ptr: new(PP), + err: &UnmarshalTypeError{ + Value: "string", + Struct: "T", + Field: "Ts.Y", + Type: reflect.TypeOf(int(0)), + Offset: 29, + }, + }, +} + +func TestMarshal(t *testing.T) { + b, err := Marshal(allValue) + if err != nil { + t.Fatalf("Marshal allValue: %v", err) + } + if string(b) != allValueCompact { + t.Errorf("Marshal allValueCompact") + diff(t, b, []byte(allValueCompact)) + return + } + + b, err = Marshal(pallValue) + if err != nil { + t.Fatalf("Marshal pallValue: %v", err) + } + if string(b) != pallValueCompact { + t.Errorf("Marshal pallValueCompact") + diff(t, b, []byte(pallValueCompact)) + return + } +} + +var badUTF8 = []struct { + in, out string +}{ + {"hello\xffworld", `"hello\ufffdworld"`}, + {"", `""`}, + {"\xff", `"\ufffd"`}, + {"\xff\xff", `"\ufffd\ufffd"`}, + {"a\xffb", `"a\ufffdb"`}, + {"\xe6\x97\xa5\xe6\x9c\xac\xff\xaa\x9e", `"ๆ—ฅๆœฌ\ufffd\ufffd\ufffd"`}, +} + +func TestMarshalBadUTF8(t *testing.T) { + for _, tt := range badUTF8 { + b, err := Marshal(tt.in) + if string(b) != tt.out || err != nil { + t.Errorf("Marshal(%q) = %#q, %v, want %#q, nil", tt.in, b, err, tt.out) + } + } +} + +func TestMarshalNumberZeroVal(t *testing.T) { + var n Number + out, err := Marshal(n) + if err != nil { + t.Fatal(err) + } + outStr := string(out) + if outStr != "0" { + t.Fatalf("Invalid zero val for Number: %q", outStr) + } +} + +func TestMarshalEmbeds(t *testing.T) { + top := &Top{ + Level0: 1, + Embed0: Embed0{ + Level1b: 2, + Level1c: 3, + }, + Embed0a: &Embed0a{ + Level1a: 5, + Level1b: 6, + }, + Embed0b: &Embed0b{ + Level1a: 8, + Level1b: 9, + Level1c: 10, + Level1d: 11, + Level1e: 12, + }, + Loop: Loop{ + Loop1: 13, + Loop2: 14, + }, + Embed0p: Embed0p{ + Point: image.Point{X: 15, Y: 16}, + }, + Embed0q: Embed0q{ + Point: Point{Z: 17}, + }, + embed: embed{ + Q: 18, + }, + } + b, err := Marshal(top) + if err != nil { + t.Fatal(err) + } + want := "{\"Level0\":1,\"Level1b\":2,\"Level1c\":3,\"Level1a\":5,\"LEVEL1B\":6,\"e\":{\"Level1a\":8,\"Level1b\":9,\"Level1c\":10,\"Level1d\":11,\"x\":12},\"Loop1\":13,\"Loop2\":14,\"X\":15,\"Y\":16,\"Z\":17,\"Q\":18}" + if string(b) != want { + t.Errorf("Wrong marshal result.\n got: %q\nwant: %q", b, want) + } +} + +func equalError(a, b error) bool { + if a == nil { + return b == nil + } + if b == nil { + return a == nil + } + return a.Error() == b.Error() +} + +func TestUnmarshal(t *testing.T) { + for i, tt := range unmarshalTests { + var scan scanner + in := []byte(tt.in) + if err := checkValid(in, &scan); err != nil { + if !equalError(err, tt.err) { + t.Errorf("#%d: checkValid: %#v", i, err) + continue + } + } + if tt.ptr == nil { + continue + } + + // v = new(right-type) + v := reflect.New(reflect.TypeOf(tt.ptr).Elem()) + dec := NewDecoder(bytes.NewReader(in)) + if tt.useNumber { + dec.UseNumber() + } + if tt.disallowUnknownFields { + dec.DisallowUnknownFields() + } + err := dec.Decode(v.Interface()) + assertErrorPresence(t, tt.err, err, "testUnmarshal", i, tt.in) + if err != nil { + continue + } + if !reflect.DeepEqual(v.Elem().Interface(), tt.out) { + t.Errorf("#%d: mismatch\n%v\nhave: %#+v\nwant: %#+v", i, tt.in, v.Elem().Interface(), tt.out) + data, _ := Marshal(v.Elem().Interface()) + println(string(data)) + data, _ = Marshal(tt.out) + println(string(data)) + continue + } + + // Check round trip also decodes correctly. + if tt.err == nil { + enc, err := Marshal(v.Interface()) + if err != nil { + t.Errorf("#%d: error re-marshaling: %v", i, err) + continue + } + if tt.golden && !bytes.Equal(enc, in) { + t.Errorf("#%d: remarshal mismatch:\nhave: %s\nwant: %s", i, enc, in) + } + vv := reflect.New(reflect.TypeOf(tt.ptr).Elem()) + dec = NewDecoder(bytes.NewReader(enc)) + if tt.useNumber { + dec.UseNumber() + } + if err := dec.Decode(vv.Interface()); err != nil { + t.Errorf("#%d: error re-unmarshaling %#q: %v", i, enc, err) + continue + } + if !reflect.DeepEqual(v.Elem().Interface(), vv.Elem().Interface()) { + t.Errorf("#%d: mismatch\nhave: %#+v\nwant: %#+v", i, v.Elem().Interface(), vv.Elem().Interface()) + t.Errorf(" In: %q", strings.Map(noSpace, string(in))) + t.Errorf("Marshal: %q", strings.Map(noSpace, string(enc))) + continue + } + } + } +} + +func TestUnmarshalMarshal(t *testing.T) { + initBig() + var v interface{} + if err := Unmarshal(jsonBig, &v); err != nil { + t.Fatalf("Unmarshal: %v", err) + } + b, err := Marshal(v) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + if !bytes.Equal(jsonBig, b) { + t.Errorf("Marshal jsonBig") + diff(t, b, jsonBig) + return + } +} + +var numberTests = []struct { + in string + i int64 + intErr string + f float64 + floatErr string +}{ + {in: "-1.23e1", intErr: "strconv.ParseInt: parsing \"-1.23e1\": invalid syntax", f: -1.23e1}, + {in: "-12", i: -12, f: -12.0}, + {in: "1e1000", intErr: "strconv.ParseInt: parsing \"1e1000\": invalid syntax", floatErr: "strconv.ParseFloat: parsing \"1e1000\": value out of range"}, +} + +// Independent of Decode, basic coverage of the accessors in Number +func TestNumberAccessors(t *testing.T) { + for _, tt := range numberTests { + n := Number(tt.in) + if s := n.String(); s != tt.in { + t.Errorf("Number(%q).String() is %q", tt.in, s) + } + if i, err := n.Int64(); err == nil && tt.intErr == "" && i != tt.i { + t.Errorf("Number(%q).Int64() is %d", tt.in, i) + } else if (err == nil && tt.intErr != "") || (err != nil && err.Error() != tt.intErr) { + t.Errorf("Number(%q).Int64() wanted error %q but got: %v", tt.in, tt.intErr, err) + } + if f, err := n.Float64(); err == nil && tt.floatErr == "" && f != tt.f { + t.Errorf("Number(%q).Float64() is %g", tt.in, f) + } else if (err == nil && tt.floatErr != "") || (err != nil && err.Error() != tt.floatErr) { + t.Errorf("Number(%q).Float64() wanted error %q but got: %v", tt.in, tt.floatErr, err) + } + } +} + +func TestLargeByteSlice(t *testing.T) { + s0 := make([]byte, 2000) + for i := range s0 { + s0[i] = byte(i) + } + b, err := Marshal(s0) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + var s1 []byte + if err := Unmarshal(b, &s1); err != nil { + t.Fatalf("Unmarshal: %v", err) + } + if !bytes.Equal(s0, s1) { + t.Errorf("Marshal large byte slice") + diff(t, s0, s1) + } +} + +type Xint struct { + X int +} + +func TestUnmarshalInterface(t *testing.T) { + var xint Xint + var i interface{} = &xint + if err := Unmarshal([]byte(`{"X":1}`), &i); err != nil { + t.Fatalf("Unmarshal: %v", err) + } + if xint.X != 1 { + t.Fatalf("Did not write to xint") + } +} + +func TestUnmarshalPtrPtr(t *testing.T) { + var xint Xint + pxint := &xint + if err := Unmarshal([]byte(`{"X":1}`), &pxint); err != nil { + t.Fatalf("Unmarshal: %v", err) + } + if xint.X != 1 { + t.Fatalf("Did not write to xint") + } +} + +func TestEscape(t *testing.T) { + const input = `"foobar"` + " [\u2028 \u2029]" + const expected = `"\"foobar\"\u003chtml\u003e [\u2028 \u2029]"` + b, err := Marshal(input) + if err != nil { + t.Fatalf("Marshal error: %v", err) + } + if s := string(b); s != expected { + t.Errorf("Encoding of [%s]:\n got [%s]\nwant [%s]", input, s, expected) + } +} + +// WrongString is a struct that's misusing the ,string modifier. +type WrongString struct { + Message string `json:"result,string"` +} + +type wrongStringTest struct { + in, err string +} + +var wrongStringTests = []wrongStringTest{ + {`{"result":"x"}`, `json: invalid use of ,string struct tag, trying to unmarshal "x" into string`}, + {`{"result":"foo"}`, `json: invalid use of ,string struct tag, trying to unmarshal "foo" into string`}, + {`{"result":"123"}`, `json: invalid use of ,string struct tag, trying to unmarshal "123" into string`}, + {`{"result":123}`, `json: invalid use of ,string struct tag, trying to unmarshal unquoted value into string`}, + {`{"result":"\""}`, `json: invalid use of ,string struct tag, trying to unmarshal "\"" into string`}, + {`{"result":"\"foo"}`, `json: invalid use of ,string struct tag, trying to unmarshal "\"foo" into string`}, +} + +// If people misuse the ,string modifier, the error message should be +// helpful, telling the user that they're doing it wrong. +func TestErrorMessageFromMisusedString(t *testing.T) { + for n, tt := range wrongStringTests { + r := strings.NewReader(tt.in) + var s WrongString + err := NewDecoder(r).Decode(&s) + assertErrorPresence(t, errors.New(tt.err), err, n) + } +} + +func noSpace(c rune) rune { + if isSpace(byte(c)) { //only used for ascii + return -1 + } + return c +} + +type All struct { + Bool bool + Int int + Int8 int8 + Int16 int16 + Int32 int32 + Int64 int64 + Uint uint + Uint8 uint8 + Uint16 uint16 + Uint32 uint32 + Uint64 uint64 + Uintptr uintptr + Float32 float32 + Float64 float64 + + Foo string `json:"bar"` + Foo2 string `json:"bar2,dummyopt"` + + IntStr int64 `json:",string"` + UintptrStr uintptr `json:",string"` + + PBool *bool + PInt *int + PInt8 *int8 + PInt16 *int16 + PInt32 *int32 + PInt64 *int64 + PUint *uint + PUint8 *uint8 + PUint16 *uint16 + PUint32 *uint32 + PUint64 *uint64 + PUintptr *uintptr + PFloat32 *float32 + PFloat64 *float64 + + String string + PString *string + + Map map[string]Small + MapP map[string]*Small + PMap *map[string]Small + PMapP *map[string]*Small + + EmptyMap map[string]Small + NilMap map[string]Small + + Slice []Small + SliceP []*Small + PSlice *[]Small + PSliceP *[]*Small + + EmptySlice []Small + NilSlice []Small + + StringSlice []string + ByteSlice []byte + + Small Small + PSmall *Small + PPSmall **Small + + Interface interface{} + PInterface *interface{} + + unexported int +} + +type Small struct { + Tag string +} + +var allValue = All{ + Bool: true, + Int: 2, + Int8: 3, + Int16: 4, + Int32: 5, + Int64: 6, + Uint: 7, + Uint8: 8, + Uint16: 9, + Uint32: 10, + Uint64: 11, + Uintptr: 12, + Float32: 14.1, + Float64: 15.1, + Foo: "foo", + Foo2: "foo2", + IntStr: 42, + UintptrStr: 44, + String: "16", + Map: map[string]Small{ + "17": {Tag: "tag17"}, + "18": {Tag: "tag18"}, + }, + MapP: map[string]*Small{ + "19": {Tag: "tag19"}, + "20": nil, + }, + EmptyMap: map[string]Small{}, + Slice: []Small{{Tag: "tag20"}, {Tag: "tag21"}}, + SliceP: []*Small{{Tag: "tag22"}, nil, {Tag: "tag23"}}, + EmptySlice: []Small{}, + StringSlice: []string{"str24", "str25", "str26"}, + ByteSlice: []byte{27, 28, 29}, + Small: Small{Tag: "tag30"}, + PSmall: &Small{Tag: "tag31"}, + Interface: 5.2, +} + +var pallValue = All{ + PBool: &allValue.Bool, + PInt: &allValue.Int, + PInt8: &allValue.Int8, + PInt16: &allValue.Int16, + PInt32: &allValue.Int32, + PInt64: &allValue.Int64, + PUint: &allValue.Uint, + PUint8: &allValue.Uint8, + PUint16: &allValue.Uint16, + PUint32: &allValue.Uint32, + PUint64: &allValue.Uint64, + PUintptr: &allValue.Uintptr, + PFloat32: &allValue.Float32, + PFloat64: &allValue.Float64, + PString: &allValue.String, + PMap: &allValue.Map, + PMapP: &allValue.MapP, + PSlice: &allValue.Slice, + PSliceP: &allValue.SliceP, + PPSmall: &allValue.PSmall, + PInterface: &allValue.Interface, +} + +var allValueIndent = `{ + "Bool": true, + "Int": 2, + "Int8": 3, + "Int16": 4, + "Int32": 5, + "Int64": 6, + "Uint": 7, + "Uint8": 8, + "Uint16": 9, + "Uint32": 10, + "Uint64": 11, + "Uintptr": 12, + "Float32": 14.1, + "Float64": 15.1, + "bar": "foo", + "bar2": "foo2", + "IntStr": "42", + "UintptrStr": "44", + "PBool": null, + "PInt": null, + "PInt8": null, + "PInt16": null, + "PInt32": null, + "PInt64": null, + "PUint": null, + "PUint8": null, + "PUint16": null, + "PUint32": null, + "PUint64": null, + "PUintptr": null, + "PFloat32": null, + "PFloat64": null, + "String": "16", + "PString": null, + "Map": { + "17": { + "Tag": "tag17" + }, + "18": { + "Tag": "tag18" + } + }, + "MapP": { + "19": { + "Tag": "tag19" + }, + "20": null + }, + "PMap": null, + "PMapP": null, + "EmptyMap": {}, + "NilMap": null, + "Slice": [ + { + "Tag": "tag20" + }, + { + "Tag": "tag21" + } + ], + "SliceP": [ + { + "Tag": "tag22" + }, + null, + { + "Tag": "tag23" + } + ], + "PSlice": null, + "PSliceP": null, + "EmptySlice": [], + "NilSlice": null, + "StringSlice": [ + "str24", + "str25", + "str26" + ], + "ByteSlice": "Gxwd", + "Small": { + "Tag": "tag30" + }, + "PSmall": { + "Tag": "tag31" + }, + "PPSmall": null, + "Interface": 5.2, + "PInterface": null +}` + +var allValueCompact = strings.Map(noSpace, allValueIndent) + +var pallValueIndent = `{ + "Bool": false, + "Int": 0, + "Int8": 0, + "Int16": 0, + "Int32": 0, + "Int64": 0, + "Uint": 0, + "Uint8": 0, + "Uint16": 0, + "Uint32": 0, + "Uint64": 0, + "Uintptr": 0, + "Float32": 0, + "Float64": 0, + "bar": "", + "bar2": "", + "IntStr": "0", + "UintptrStr": "0", + "PBool": true, + "PInt": 2, + "PInt8": 3, + "PInt16": 4, + "PInt32": 5, + "PInt64": 6, + "PUint": 7, + "PUint8": 8, + "PUint16": 9, + "PUint32": 10, + "PUint64": 11, + "PUintptr": 12, + "PFloat32": 14.1, + "PFloat64": 15.1, + "String": "", + "PString": "16", + "Map": null, + "MapP": null, + "PMap": { + "17": { + "Tag": "tag17" + }, + "18": { + "Tag": "tag18" + } + }, + "PMapP": { + "19": { + "Tag": "tag19" + }, + "20": null + }, + "EmptyMap": null, + "NilMap": null, + "Slice": null, + "SliceP": null, + "PSlice": [ + { + "Tag": "tag20" + }, + { + "Tag": "tag21" + } + ], + "PSliceP": [ + { + "Tag": "tag22" + }, + null, + { + "Tag": "tag23" + } + ], + "EmptySlice": null, + "NilSlice": null, + "StringSlice": null, + "ByteSlice": null, + "Small": { + "Tag": "" + }, + "PSmall": null, + "PPSmall": { + "Tag": "tag31" + }, + "Interface": null, + "PInterface": 5.2 +}` + +var pallValueCompact = strings.Map(noSpace, pallValueIndent) + +func TestRefUnmarshal(t *testing.T) { + type S struct { + // Ref is defined in encode_test.go. + R0 Ref + R1 *Ref + R2 RefText + R3 *RefText + } + want := S{ + R0: 12, + R1: new(Ref), + R2: 13, + R3: new(RefText), + } + *want.R1 = 12 + *want.R3 = 13 + + var got S + if err := Unmarshal([]byte(`{"R0":"ref","R1":"ref","R2":"ref","R3":"ref"}`), &got); err != nil { + t.Fatalf("Unmarshal: %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("got %+v, want %+v", got, want) + } +} + +// Test that the empty string doesn't panic decoding when ,string is specified +// Issue 3450 +func TestEmptyString(t *testing.T) { + type T2 struct { + Number1 int `json:",string"` + Number2 int `json:",string"` + } + data := `{"Number1":"1", "Number2":""}` + dec := NewDecoder(strings.NewReader(data)) + var t2 T2 + err := dec.Decode(&t2) + if err == nil { + t.Fatal("Decode: did not return error") + } + if t2.Number1 != 1 { + t.Fatal("Decode: did not set Number1") + } +} + +// Test that a null for ,string is not replaced with the previous quoted string (issue 7046). +// It should also not be an error (issue 2540, issue 8587). +func TestNullString(t *testing.T) { + type T struct { + A int `json:",string"` + B int `json:",string"` + C *int `json:",string"` + } + data := []byte(`{"A": "1", "B": null, "C": null}`) + var s T + s.B = 1 + s.C = new(int) + *s.C = 2 + err := Unmarshal(data, &s) + if err != nil { + t.Fatalf("Unmarshal: %v", err) + } + if s.B != 1 || s.C != nil { + t.Fatalf("after Unmarshal, s.B=%d, s.C=%p, want 1, nil", s.B, s.C) + } +} + +func intp(x int) *int { + p := new(int) + *p = x + return p +} + +func intpp(x *int) **int { + pp := new(*int) + *pp = x + return pp +} + +var interfaceSetTests = []struct { + pre interface{} + json string + post interface{} +}{ + {"foo", `"bar"`, "bar"}, + {"foo", `2`, 2.0}, + {"foo", `true`, true}, + {"foo", `null`, nil}, + + {nil, `null`, nil}, + {new(int), `null`, nil}, + {(*int)(nil), `null`, nil}, + {new(*int), `null`, new(*int)}, + {(**int)(nil), `null`, nil}, + {intp(1), `null`, nil}, + {intpp(nil), `null`, intpp(nil)}, + {intpp(intp(1)), `null`, intpp(nil)}, +} + +func TestInterfaceSet(t *testing.T) { + for _, tt := range interfaceSetTests { + b := struct{ X interface{} }{tt.pre} + blob := `{"X":` + tt.json + `}` + if err := Unmarshal([]byte(blob), &b); err != nil { + t.Errorf("Unmarshal %#q: %v", blob, err) + continue + } + if !reflect.DeepEqual(b.X, tt.post) { + t.Errorf("Unmarshal %#q into %#v: X=%#v, want %#v", blob, tt.pre, b.X, tt.post) + } + } +} + +type NullTest struct { + Bool bool + Int int + Int8 int8 + Int16 int16 + Int32 int32 + Int64 int64 + Uint uint + Uint8 uint8 + Uint16 uint16 + Uint32 uint32 + Uint64 uint64 + Float32 float32 + Float64 float64 + String string + PBool *bool + Map map[string]string + Slice []string + Interface interface{} + + PRaw *RawMessage + PTime *time.Time + PBigInt *big.Int + PText *MustNotUnmarshalText + PBuffer *bytes.Buffer // has methods, just not relevant ones + PStruct *struct{} + + Raw RawMessage + Time time.Time + BigInt big.Int + Text MustNotUnmarshalText + Buffer bytes.Buffer + Struct struct{} +} + +type NullTestStrings struct { + Bool bool `json:",string"` + Int int `json:",string"` + Int8 int8 `json:",string"` + Int16 int16 `json:",string"` + Int32 int32 `json:",string"` + Int64 int64 `json:",string"` + Uint uint `json:",string"` + Uint8 uint8 `json:",string"` + Uint16 uint16 `json:",string"` + Uint32 uint32 `json:",string"` + Uint64 uint64 `json:",string"` + Float32 float32 `json:",string"` + Float64 float64 `json:",string"` + String string `json:",string"` + PBool *bool `json:",string"` + Map map[string]string `json:",string"` + Slice []string `json:",string"` + Interface interface{} `json:",string"` + + PRaw *RawMessage `json:",string"` + PTime *time.Time `json:",string"` + PBigInt *big.Int `json:",string"` + PText *MustNotUnmarshalText `json:",string"` + PBuffer *bytes.Buffer `json:",string"` + PStruct *struct{} `json:",string"` + + Raw RawMessage `json:",string"` + Time time.Time `json:",string"` + BigInt big.Int `json:",string"` + Text MustNotUnmarshalText `json:",string"` + Buffer bytes.Buffer `json:",string"` + Struct struct{} `json:",string"` +} + +// JSON null values should be ignored for primitives and string values instead of resulting in an error. +// Issue 2540 +func TestUnmarshalNulls(t *testing.T) { + // Unmarshal docs: + // The JSON null value unmarshals into an interface, map, pointer, or slice + // by setting that Go value to nil. Because null is often used in JSON to mean + // ``not present,'' unmarshaling a JSON null into any other Go type has no effect + // on the value and produces no error. + + jsonData := []byte(`{ + "Bool" : null, + "Int" : null, + "Int8" : null, + "Int16" : null, + "Int32" : null, + "Int64" : null, + "Uint" : null, + "Uint8" : null, + "Uint16" : null, + "Uint32" : null, + "Uint64" : null, + "Float32" : null, + "Float64" : null, + "String" : null, + "PBool": null, + "Map": null, + "Slice": null, + "Interface": null, + "PRaw": null, + "PTime": null, + "PBigInt": null, + "PText": null, + "PBuffer": null, + "PStruct": null, + "Raw": null, + "Time": null, + "BigInt": null, + "Text": null, + "Buffer": null, + "Struct": null + }`) + nulls := NullTest{ + Bool: true, + Int: 2, + Int8: 3, + Int16: 4, + Int32: 5, + Int64: 6, + Uint: 7, + Uint8: 8, + Uint16: 9, + Uint32: 10, + Uint64: 11, + Float32: 12.1, + Float64: 13.1, + String: "14", + PBool: new(bool), + Map: map[string]string{}, + Slice: []string{}, + Interface: new(MustNotUnmarshalJSON), + PRaw: new(RawMessage), + PTime: new(time.Time), + PBigInt: new(big.Int), + PText: new(MustNotUnmarshalText), + PStruct: new(struct{}), + PBuffer: new(bytes.Buffer), + Raw: RawMessage("123"), + Time: time.Unix(123456789, 0), + BigInt: *big.NewInt(123), + } + + before := nulls.Time.String() + + err := Unmarshal(jsonData, &nulls) + if err != nil { + t.Errorf("Unmarshal of null values failed: %v", err) + } + if !nulls.Bool || nulls.Int != 2 || nulls.Int8 != 3 || nulls.Int16 != 4 || nulls.Int32 != 5 || nulls.Int64 != 6 || + nulls.Uint != 7 || nulls.Uint8 != 8 || nulls.Uint16 != 9 || nulls.Uint32 != 10 || nulls.Uint64 != 11 || + nulls.Float32 != 12.1 || nulls.Float64 != 13.1 || nulls.String != "14" { + t.Errorf("Unmarshal of null values affected primitives") + } + + if nulls.PBool != nil { + t.Errorf("Unmarshal of null did not clear nulls.PBool") + } + if nulls.Map != nil { + t.Errorf("Unmarshal of null did not clear nulls.Map") + } + if nulls.Slice != nil { + t.Errorf("Unmarshal of null did not clear nulls.Slice") + } + if nulls.Interface != nil { + t.Errorf("Unmarshal of null did not clear nulls.Interface") + } + if nulls.PRaw != nil { + t.Errorf("Unmarshal of null did not clear nulls.PRaw") + } + if nulls.PTime != nil { + t.Errorf("Unmarshal of null did not clear nulls.PTime") + } + if nulls.PBigInt != nil { + t.Errorf("Unmarshal of null did not clear nulls.PBigInt") + } + if nulls.PText != nil { + t.Errorf("Unmarshal of null did not clear nulls.PText") + } + if nulls.PBuffer != nil { + t.Errorf("Unmarshal of null did not clear nulls.PBuffer") + } + if nulls.PStruct != nil { + t.Errorf("Unmarshal of null did not clear nulls.PStruct") + } + + if string(nulls.Raw) != "null" { + t.Errorf("Unmarshal of RawMessage null did not record null: %v", string(nulls.Raw)) + } + if nulls.Time.String() != before { + t.Errorf("Unmarshal of time.Time null set time to %v", nulls.Time.String()) + } + if nulls.BigInt.String() != "123" { + t.Errorf("Unmarshal of big.Int null set int to %v", nulls.BigInt.String()) + } +} + +type MustNotUnmarshalJSON struct{} + +func (x MustNotUnmarshalJSON) UnmarshalJSON(data []byte) error { + return errors.New("MustNotUnmarshalJSON was used") +} + +type MustNotUnmarshalText struct{} + +func (x MustNotUnmarshalText) UnmarshalText(text []byte) error { + return errors.New("MustNotUnmarshalText was used") +} + +func TestStringKind(t *testing.T) { + type stringKind string + + var m1, m2 map[stringKind]int + m1 = map[stringKind]int{ + "foo": 42, + } + + data, err := Marshal(m1) + if err != nil { + t.Errorf("Unexpected error marshaling: %v", err) + } + + err = Unmarshal(data, &m2) + if err != nil { + t.Errorf("Unexpected error unmarshaling: %v", err) + } + + if !reflect.DeepEqual(m1, m2) { + t.Error("Items should be equal after encoding and then decoding") + } +} + +// Custom types with []byte as underlying type could not be marshaled +// and then unmarshaled. +// Issue 8962. +func TestByteKind(t *testing.T) { + type byteKind []byte + + a := byteKind("hello") + + data, err := Marshal(a) + if err != nil { + t.Error(err) + } + var b byteKind + err = Unmarshal(data, &b) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(a, b) { + t.Errorf("expected %v == %v", a, b) + } +} + +// The fix for issue 8962 introduced a regression. +// Issue 12921. +func TestSliceOfCustomByte(t *testing.T) { + type Uint8 uint8 + + a := []Uint8("hello") + + data, err := Marshal(a) + if err != nil { + t.Fatal(err) + } + var b []Uint8 + err = Unmarshal(data, &b) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(a, b) { + t.Fatalf("expected %v == %v", a, b) + } +} + +var decodeTypeErrorTests = []struct { + dest interface{} + src string +}{ + {new(string), `{"user": "name"}`}, // issue 4628. + {new(error), `{}`}, // issue 4222 + {new(error), `[]`}, + {new(error), `""`}, + {new(error), `123`}, + {new(error), `true`}, +} + +func TestUnmarshalTypeError(t *testing.T) { + for _, item := range decodeTypeErrorTests { + err := Unmarshal([]byte(item.src), item.dest) + if _, ok := err.(*UnmarshalTypeError); !ok { + t.Errorf("expected type error for Unmarshal(%q, type %T): got %T", + item.src, item.dest, err) + } + } +} + +var unmarshalSyntaxTests = []string{ + "tru", + "fals", + "nul", + "123e", + `"hello`, + `[1,2,3`, + `{"key":1`, + `{"key":1,`, +} + +func TestUnmarshalSyntax(t *testing.T) { + var x interface{} + for _, src := range unmarshalSyntaxTests { + err := Unmarshal([]byte(src), &x) + if _, ok := err.(*SyntaxError); !ok { + t.Errorf("expected syntax error for Unmarshal(%q): got %T", src, err) + } + } +} + +// Test handling of unexported fields that should be ignored. +// Issue 4660 +type unexportedFields struct { + Name string + m map[string]interface{} `json:"-"` + m2 map[string]interface{} `json:"abcd"` + + s []int `json:"-"` +} + +func TestUnmarshalUnexported(t *testing.T) { + input := `{"Name": "Bob", "m": {"x": 123}, "m2": {"y": 456}, "abcd": {"z": 789}, "s": [2, 3]}` + want := &unexportedFields{Name: "Bob"} + + out := &unexportedFields{} + err := Unmarshal([]byte(input), out) + if err != nil { + t.Errorf("got error %v, expected nil", err) + } + if !reflect.DeepEqual(out, want) { + t.Errorf("got %q, want %q", out, want) + } +} + +// Time3339 is a time.Time which encodes to and from JSON +// as an RFC 3339 time in UTC. +type Time3339 time.Time + +func (t *Time3339) UnmarshalJSON(b []byte) error { + if len(b) < 2 || b[0] != '"' || b[len(b)-1] != '"' { + return fmt.Errorf("types: failed to unmarshal non-string value %q as an RFC 3339 time", b) + } + tm, err := time.Parse(time.RFC3339, string(b[1:len(b)-1])) + if err != nil { + return err + } + *t = Time3339(tm) + return nil +} + +func TestUnmarshalJSONLiteralError(t *testing.T) { + var t3 Time3339 + err := Unmarshal([]byte(`"0000-00-00T00:00:00Z"`), &t3) + if err == nil { + t.Fatalf("expected error; got time %v", time.Time(t3)) + } + if !strings.Contains(err.Error(), "range") { + t.Errorf("got err = %v; want out of range error", err) + } +} + +// Test that extra object elements in an array do not result in a +// "data changing underfoot" error. +// Issue 3717 +func TestSkipArrayObjects(t *testing.T) { + json := `[{}]` + var dest [0]interface{} + + err := Unmarshal([]byte(json), &dest) + if err != nil { + t.Errorf("got error %q, want nil", err) + } +} + +// Test semantics of pre-filled struct fields and pre-filled map fields. +// Issue 4900. +func TestPrefilled(t *testing.T) { + ptrToMap := func(m map[string]interface{}) *map[string]interface{} { return &m } + + // Values here change, cannot reuse table across runs. + var prefillTests = []struct { + in string + ptr interface{} + out interface{} + }{ + { + in: `{"X": 1, "Y": 2}`, + ptr: &XYZ{X: float32(3), Y: int16(4), Z: 1.5}, + out: &XYZ{X: float64(1), Y: float64(2), Z: 1.5}, + }, + { + in: `{"X": 1, "Y": 2}`, + ptr: ptrToMap(map[string]interface{}{"X": float32(3), "Y": int16(4), "Z": 1.5}), + out: ptrToMap(map[string]interface{}{"X": float64(1), "Y": float64(2), "Z": 1.5}), + }, + } + + for _, tt := range prefillTests { + ptrstr := fmt.Sprintf("%v", tt.ptr) + err := Unmarshal([]byte(tt.in), tt.ptr) // tt.ptr edited here + if err != nil { + t.Errorf("Unmarshal: %v", err) + } + if !reflect.DeepEqual(tt.ptr, tt.out) { + t.Errorf("Unmarshal(%#q, %s): have %v, want %v", tt.in, ptrstr, tt.ptr, tt.out) + } + } +} + +var invalidUnmarshalTests = []struct { + v interface{} + want string +}{ + {nil, "json: Unmarshal(nil)"}, + {struct{}{}, "json: Unmarshal(non-pointer struct {})"}, + {(*int)(nil), "json: Unmarshal(nil *int)"}, +} + +func TestInvalidUnmarshal(t *testing.T) { + buf := []byte(`{"a":"1"}`) + for _, tt := range invalidUnmarshalTests { + err := Unmarshal(buf, tt.v) + if err == nil { + t.Errorf("Unmarshal expecting error, got nil") + continue + } + if got := err.Error(); got != tt.want { + t.Errorf("Unmarshal = %q; want %q", got, tt.want) + } + } +} + +var invalidUnmarshalTextTests = []struct { + v interface{} + want string +}{ + {nil, "json: Unmarshal(nil)"}, + {struct{}{}, "json: Unmarshal(non-pointer struct {})"}, + {(*int)(nil), "json: Unmarshal(nil *int)"}, + {new(net.IP), "json: cannot unmarshal number into Go value of type *net.IP"}, +} + +func TestInvalidUnmarshalText(t *testing.T) { + buf := []byte(`123`) + for _, tt := range invalidUnmarshalTextTests { + err := Unmarshal(buf, tt.v) + if err == nil { + t.Errorf("Unmarshal expecting error, got nil") + continue + } + if got := err.Error(); got != tt.want { + t.Errorf("Unmarshal = %q; want %q", got, tt.want) + } + } +} + +// Test that string option is ignored for invalid types. +// Issue 9812. +func TestInvalidStringOption(t *testing.T) { + num := 0 + item := struct { + T time.Time `json:",string"` + M map[string]string `json:",string"` + S []string `json:",string"` + A [1]string `json:",string"` + I interface{} `json:",string"` + P *int `json:",string"` + }{M: make(map[string]string), S: make([]string, 0), I: num, P: &num} + + data, err := Marshal(item) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + + err = Unmarshal(data, &item) + if err != nil { + t.Fatalf("Unmarshal: %v", err) + } +} + +// Test unmarshal behavior with regards to embedded unexported structs. +// +// (Issue 21357) If the embedded struct is a pointer and is unallocated, +// this returns an error because unmarshal cannot set the field. +// +// (Issue 24152) If the embedded struct is given an explicit name, +// ensure that the normal unmarshal logic does not panic in reflect. +// +// (Issue 28145) If the embedded struct is given an explicit name and has +// exported methods, don't cause a panic trying to get its value. +func TestUnmarshalEmbeddedUnexported(t *testing.T) { + type ( + embed1 struct{ Q int } + embed2 struct{ Q int } + embed3 struct { + Q int64 `json:",string"` + } + S1 struct { + *embed1 + R int + } + S2 struct { + *embed1 + Q int + } + S3 struct { + embed1 + R int + } + S4 struct { + *embed1 + embed2 + } + S5 struct { + *embed3 + R int + } + S6 struct { + embed1 `json:"embed1"` + } + S7 struct { + embed1 `json:"embed1"` + embed2 + } + S8 struct { + embed1 `json:"embed1"` + embed2 `json:"embed2"` + Q int + } + S9 struct { + unexportedWithMethods `json:"embed"` + } + ) + + tests := []struct { + in string + ptr interface{} + out interface{} + err error + }{{ + // Error since we cannot set S1.embed1, but still able to set S1.R. + in: `{"R":2,"Q":1}`, + ptr: new(S1), + out: &S1{R: 2}, + err: fmt.Errorf("json: cannot set embedded pointer to unexported struct: json.embed1"), + }, { + // The top level Q field takes precedence. + in: `{"Q":1}`, + ptr: new(S2), + out: &S2{Q: 1}, + }, { + // No issue with non-pointer variant. + in: `{"R":2,"Q":1}`, + ptr: new(S3), + out: &S3{embed1: embed1{Q: 1}, R: 2}, + }, { + // No error since both embedded structs have field R, which annihilate each other. + // Thus, no attempt is made at setting S4.embed1. + in: `{"R":2}`, + ptr: new(S4), + out: new(S4), + }, { + // Error since we cannot set S5.embed1, but still able to set S5.R. + in: `{"R":2,"Q":1}`, + ptr: new(S5), + out: &S5{R: 2}, + err: fmt.Errorf("json: cannot set embedded pointer to unexported struct: json.embed3"), + }, { + // Issue 24152, ensure decodeState.indirect does not panic. + in: `{"embed1": {"Q": 1}}`, + ptr: new(S6), + out: &S6{embed1{1}}, + }, { + // Issue 24153, check that we can still set forwarded fields even in + // the presence of a name conflict. + // + // This relies on obscure behavior of reflect where it is possible + // to set a forwarded exported field on an unexported embedded struct + // even though there is a name conflict, even when it would have been + // impossible to do so according to Go visibility rules. + // Go forbids this because it is ambiguous whether S7.Q refers to + // S7.embed1.Q or S7.embed2.Q. Since embed1 and embed2 are unexported, + // it should be impossible for an external package to set either Q. + // + // It is probably okay for a future reflect change to break this. + in: `{"embed1": {"Q": 1}, "Q": 2}`, + ptr: new(S7), + out: &S7{embed1{1}, embed2{2}}, + }, { + // Issue 24153, similar to the S7 case. + in: `{"embed1": {"Q": 1}, "embed2": {"Q": 2}, "Q": 3}`, + ptr: new(S8), + out: &S8{embed1{1}, embed2{2}, 3}, + }, { + // Issue 228145, similar to the cases above. + in: `{"embed": {}}`, + ptr: new(S9), + out: &S9{}, + }} + + for i, tt := range tests { + err := Unmarshal([]byte(tt.in), tt.ptr) + if !equalError(err, tt.err) { + t.Errorf("#%d: %v, want %v", i, err, tt.err) + } + if !reflect.DeepEqual(tt.ptr, tt.out) { + t.Errorf("#%d: mismatch\ngot: %#+v\nwant: %#+v", i, tt.ptr, tt.out) + } + } +} + +type unmarshalPanic struct{} + +func (unmarshalPanic) UnmarshalJSON([]byte) error { panic(0xdead) } + +func TestUnmarshalPanic(t *testing.T) { + defer func() { + if got := recover(); !reflect.DeepEqual(got, 0xdead) { + t.Errorf("panic() = (%T)(%v), want 0xdead", got, got) + } + }() + Unmarshal([]byte("{}"), &unmarshalPanic{}) + t.Fatalf("Unmarshal should have panicked") +} + +// The decoder used to hang if decoding into an interface pointing to its own address. +// See golang.org/issues/31740. +func TestUnmarshalRecursivePointer(t *testing.T) { + var v interface{} + v = &v + data := []byte(`{"a": "b"}`) + + if err := Unmarshal(data, v); err != nil { + t.Fatal(err) + } +} diff --git a/utils/encoding/json/golang_encode_test.go b/utils/encoding/json/golang_encode_test.go new file mode 100644 index 00000000..b31b6a47 --- /dev/null +++ b/utils/encoding/json/golang_encode_test.go @@ -0,0 +1,1033 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package json + +import ( + "bytes" + "fmt" + "log" + "math" + "reflect" + "regexp" + "strconv" + "testing" + "unicode" +) + +type Optionals struct { + Sr string `json:"sr"` + So string `json:"so,omitempty"` + Sw string `json:"-"` + + Ir int `json:"omitempty"` // actually named omitempty, not an option + Io int `json:"io,omitempty"` + + Slr []string `json:"slr,random"` + Slo []string `json:"slo,omitempty"` + + Mr map[string]interface{} `json:"mr"` + Mo map[string]interface{} `json:",omitempty"` + + Fr float64 `json:"fr"` + Fo float64 `json:"fo,omitempty"` + + Br bool `json:"br"` + Bo bool `json:"bo,omitempty"` + + Ur uint `json:"ur"` + Uo uint `json:"uo,omitempty"` + + Str struct{} `json:"str"` + Sto struct{} `json:"sto,omitempty"` +} + +var optionalsExpected = `{ + "sr": "", + "omitempty": 0, + "slr": null, + "mr": {}, + "fr": 0, + "br": false, + "ur": 0, + "str": {}, + "sto": {} +}` + +func TestOmitEmpty(t *testing.T) { + var o Optionals + o.Sw = "something" + o.Mr = map[string]interface{}{} + o.Mo = map[string]interface{}{} + + got, err := MarshalIndent(&o, "", " ") + if err != nil { + t.Fatal(err) + } + if got := string(got); got != optionalsExpected { + t.Errorf(" got: %s\nwant: %s\n", got, optionalsExpected) + } +} + +type StringTag struct { + BoolStr bool `json:",string"` + IntStr int64 `json:",string"` + UintptrStr uintptr `json:",string"` + StrStr string `json:",string"` +} + +var stringTagExpected = `{ + "BoolStr": "true", + "IntStr": "42", + "UintptrStr": "44", + "StrStr": "\"xzbit\"" +}` + +func TestStringTag(t *testing.T) { + var s StringTag + s.BoolStr = true + s.IntStr = 42 + s.UintptrStr = 44 + s.StrStr = "xzbit" + got, err := MarshalIndent(&s, "", " ") + if err != nil { + t.Fatal(err) + } + if got := string(got); got != stringTagExpected { + t.Fatalf(" got: %s\nwant: %s\n", got, stringTagExpected) + } + + // Verify that it round-trips. + var s2 StringTag + err = NewDecoder(bytes.NewReader(got)).Decode(&s2) + if err != nil { + t.Fatalf("Decode: %v", err) + } + if !reflect.DeepEqual(s, s2) { + t.Fatalf("decode didn't match.\nsource: %#v\nEncoded as:\n%s\ndecode: %#v", s, string(got), s2) + } +} + +// byte slices are special even if they're renamed types. +type renamedByte byte +type renamedByteSlice []byte +type renamedRenamedByteSlice []renamedByte + +func TestEncodeRenamedByteSlice(t *testing.T) { + s := renamedByteSlice("abc") + result, err := Marshal(s) + if err != nil { + t.Fatal(err) + } + expect := `"YWJj"` + if string(result) != expect { + t.Errorf(" got %s want %s", result, expect) + } + r := renamedRenamedByteSlice("abc") + result, err = Marshal(r) + if err != nil { + t.Fatal(err) + } + if string(result) != expect { + t.Errorf(" got %s want %s", result, expect) + } +} + +var unsupportedValues = []interface{}{ + math.NaN(), + math.Inf(-1), + math.Inf(1), +} + +func TestUnsupportedValues(t *testing.T) { + for _, v := range unsupportedValues { + if _, err := Marshal(v); err != nil { + if _, ok := err.(*UnsupportedValueError); !ok { + t.Errorf("for %v, got %T want UnsupportedValueError", v, err) + } + } else { + t.Errorf("for %v, expected error", v) + } + } +} + +// Ref has Marshaler and Unmarshaler methods with pointer receiver. +type Ref int + +func (*Ref) MarshalJSON() ([]byte, error) { + return []byte(`"ref"`), nil +} + +func (r *Ref) UnmarshalJSON([]byte) error { + *r = 12 + return nil +} + +// Val has Marshaler methods with value receiver. +type Val int + +func (Val) MarshalJSON() ([]byte, error) { + return []byte(`"val"`), nil +} + +// RefText has Marshaler and Unmarshaler methods with pointer receiver. +type RefText int + +func (*RefText) MarshalText() ([]byte, error) { + return []byte(`"ref"`), nil +} + +func (r *RefText) UnmarshalText([]byte) error { + *r = 13 + return nil +} + +// ValText has Marshaler methods with value receiver. +type ValText int + +func (ValText) MarshalText() ([]byte, error) { + return []byte(`"val"`), nil +} + +func TestRefValMarshal(t *testing.T) { + var s = struct { + R0 Ref + R1 *Ref + R2 RefText + R3 *RefText + V0 Val + V1 *Val + V2 ValText + V3 *ValText + }{ + R0: 12, + R1: new(Ref), + R2: 14, + R3: new(RefText), + V0: 13, + V1: new(Val), + V2: 15, + V3: new(ValText), + } + const want = `{"R0":"ref","R1":"ref","R2":"\"ref\"","R3":"\"ref\"","V0":"val","V1":"val","V2":"\"val\"","V3":"\"val\""}` + b, err := Marshal(&s) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + if got := string(b); got != want { + t.Errorf("got %q, want %q", got, want) + } +} + +// C implements Marshaler and returns unescaped JSON. +type C int + +func (C) MarshalJSON() ([]byte, error) { + return []byte(`"<&>"`), nil +} + +// CText implements Marshaler and returns unescaped text. +type CText int + +func (CText) MarshalText() ([]byte, error) { + return []byte(`"<&>"`), nil +} + +func TestMarshalerEscaping(t *testing.T) { + var c C + want := `"\u003c\u0026\u003e"` + b, err := Marshal(c) + if err != nil { + t.Fatalf("Marshal(c): %v", err) + } + if got := string(b); got != want { + t.Errorf("Marshal(c) = %#q, want %#q", got, want) + } + + var ct CText + want = `"\"\u003c\u0026\u003e\""` + b, err = Marshal(ct) + if err != nil { + t.Fatalf("Marshal(ct): %v", err) + } + if got := string(b); got != want { + t.Errorf("Marshal(ct) = %#q, want %#q", got, want) + } +} + +func TestAnonymousFields(t *testing.T) { + tests := []struct { + label string // Test name + makeInput func() interface{} // Function to create input value + want string // Expected JSON output + }{{ + // Both S1 and S2 have a field named X. From the perspective of S, + // it is ambiguous which one X refers to. + // This should not serialize either field. + label: "AmbiguousField", + makeInput: func() interface{} { + type ( + S1 struct{ x, X int } + S2 struct{ x, X int } + S struct { + S1 + S2 + } + ) + return S{S1{1, 2}, S2{3, 4}} + }, + want: `{}`, + }, { + label: "DominantField", + // Both S1 and S2 have a field named X, but since S has an X field as + // well, it takes precedence over S1.X and S2.X. + makeInput: func() interface{} { + type ( + S1 struct{ x, X int } + S2 struct{ x, X int } + S struct { + S1 + S2 + x, X int + } + ) + return S{S1{1, 2}, S2{3, 4}, 5, 6} + }, + want: `{"X":6}`, + }, { + // Unexported embedded field of non-struct type should not be serialized. + label: "UnexportedEmbeddedInt", + makeInput: func() interface{} { + type ( + myInt int + S struct{ myInt } + ) + return S{5} + }, + want: `{}`, + }, { + // Exported embedded field of non-struct type should be serialized. + label: "ExportedEmbeddedInt", + makeInput: func() interface{} { + type ( + MyInt int + S struct{ MyInt } + ) + return S{5} + }, + want: `{"MyInt":5}`, + }, { + // Unexported embedded field of pointer to non-struct type + // should not be serialized. + label: "UnexportedEmbeddedIntPointer", + makeInput: func() interface{} { + type ( + myInt int + S struct{ *myInt } + ) + s := S{new(myInt)} + *s.myInt = 5 + return s + }, + want: `{}`, + }, { + // Exported embedded field of pointer to non-struct type + // should be serialized. + label: "ExportedEmbeddedIntPointer", + makeInput: func() interface{} { + type ( + MyInt int + S struct{ *MyInt } + ) + s := S{new(MyInt)} + *s.MyInt = 5 + return s + }, + want: `{"MyInt":5}`, + }, { + // Exported fields of embedded structs should have their + // exported fields be serialized regardless of whether the struct types + // themselves are exported. + label: "EmbeddedStruct", + makeInput: func() interface{} { + type ( + s1 struct{ x, X int } + S2 struct{ y, Y int } + S struct { + s1 + S2 + } + ) + return S{s1{1, 2}, S2{3, 4}} + }, + want: `{"X":2,"Y":4}`, + }, { + // Exported fields of pointers to embedded structs should have their + // exported fields be serialized regardless of whether the struct types + // themselves are exported. + label: "EmbeddedStructPointer", + makeInput: func() interface{} { + type ( + s1 struct{ x, X int } + S2 struct{ y, Y int } + S struct { + *s1 + *S2 + } + ) + return S{&s1{1, 2}, &S2{3, 4}} + }, + want: `{"X":2,"Y":4}`, + }, { + // Exported fields on embedded unexported structs at multiple levels + // of nesting should still be serialized. + label: "NestedStructAndInts", + makeInput: func() interface{} { + type ( + MyInt1 int + MyInt2 int + myInt int + s2 struct { + MyInt2 + myInt + } + s1 struct { + MyInt1 + myInt + s2 + } + S struct { + s1 + myInt + } + ) + return S{s1{1, 2, s2{3, 4}}, 6} + }, + want: `{"MyInt1":1,"MyInt2":3}`, + }, + { + // If an anonymous struct pointer field is nil, we should ignore + // the embedded fields behind it. Not properly doing so may + // result in the wrong output or reflect panics. + label: "EmbeddedFieldBehindNilPointer", + makeInput: func() interface{} { + type ( + S2 struct{ Field string } + S struct{ *S2 } + ) + return S{} + }, + want: `{}`, + }, + } + + for _, tt := range tests { + t.Run(tt.label, func(t *testing.T) { + b, err := Marshal(tt.makeInput()) + if err != nil { + t.Fatalf("Marshal() = %v, want nil error", err) + } + if string(b) != tt.want { + t.Fatalf("Marshal() = %q, want %q", b, tt.want) + } + }) + } +} + +type BugA struct { + S string +} + +type BugB struct { + BugA + S string +} + +type BugC struct { + S string +} + +// Legal Go: We never use the repeated embedded field (S). +type BugX struct { + A int + BugA + BugB +} + +// Issue 16042. Even if a nil interface value is passed in +// as long as it implements MarshalJSON, it should be marshaled. +type nilMarshaler string + +func (nm *nilMarshaler) MarshalJSON() ([]byte, error) { + if nm == nil { + return Marshal("0zenil0") + } + return Marshal("zenil:" + string(*nm)) +} + +// Issue 16042. +func TestNilMarshal(t *testing.T) { + testCases := []struct { + v interface{} + want string + }{ + {v: nil, want: `null`}, + {v: new(float64), want: `0`}, + {v: []interface{}(nil), want: `null`}, + {v: []string(nil), want: `null`}, + {v: map[string]string(nil), want: `null`}, + {v: []byte(nil), want: `null`}, + {v: struct{ M string }{"gopher"}, want: `{"M":"gopher"}`}, + {v: struct{ M Marshaler }{}, want: `{"M":null}`}, + {v: struct{ M Marshaler }{(*nilMarshaler)(nil)}, want: `{"M":"0zenil0"}`}, + {v: struct{ M interface{} }{(*nilMarshaler)(nil)}, want: `{"M":null}`}, + } + + for _, tt := range testCases { + out, err := Marshal(tt.v) + if err != nil || string(out) != tt.want { + t.Errorf("Marshal(%#v) = %#q, %#v, want %#q, nil", tt.v, out, err, tt.want) + continue + } + } +} + +// Issue 5245. +func TestEmbeddedBug(t *testing.T) { + v := BugB{ + BugA{"A"}, + "B", + } + b, err := Marshal(v) + if err != nil { + t.Fatal("Marshal:", err) + } + want := `{"S":"B"}` + got := string(b) + if got != want { + t.Fatalf("Marshal: got %s want %s", got, want) + } + // Now check that the duplicate field, S, does not appear. + x := BugX{ + A: 23, + } + b, err = Marshal(x) + if err != nil { + t.Fatal("Marshal:", err) + } + want = `{"A":23}` + got = string(b) + if got != want { + t.Fatalf("Marshal: got %s want %s", got, want) + } +} + +type BugD struct { // Same as BugA after tagging. + XXX string `json:"S"` +} + +// BugD's tagged S field should dominate BugA's. +type BugY struct { + BugA + BugD +} + +// Test that a field with a tag dominates untagged fields. +func TestTaggedFieldDominates(t *testing.T) { + v := BugY{ + BugA{"BugA"}, + BugD{"BugD"}, + } + b, err := Marshal(v) + if err != nil { + t.Fatal("Marshal:", err) + } + want := `{"S":"BugD"}` + got := string(b) + if got != want { + t.Fatalf("Marshal: got %s want %s", got, want) + } +} + +// There are no tags here, so S should not appear. +type BugZ struct { + BugA + BugC + BugY // Contains a tagged S field through BugD; should not dominate. +} + +func TestDuplicatedFieldDisappears(t *testing.T) { + v := BugZ{ + BugA{"BugA"}, + BugC{"BugC"}, + BugY{ + BugA{"nested BugA"}, + BugD{"nested BugD"}, + }, + } + b, err := Marshal(v) + if err != nil { + t.Fatal("Marshal:", err) + } + want := `{}` + got := string(b) + if got != want { + t.Fatalf("Marshal: got %s want %s", got, want) + } +} + +func TestStringBytes(t *testing.T) { + t.Parallel() + // Test that encodeState.stringBytes and encodeState.string use the same encoding. + var r []rune + for i := '\u0000'; i <= unicode.MaxRune; i++ { + if testing.Short() && i > 1000 { + i = unicode.MaxRune + } + r = append(r, i) + } + s := string(r) + "\xff\xff\xffhello" // some invalid UTF-8 too + + for _, escapeHTML := range []bool{true, false} { + es := &encodeState{} + es.string(s, escapeHTML) + + esBytes := &encodeState{} + esBytes.stringBytes([]byte(s), escapeHTML) + + enc := es.Buffer.String() + encBytes := esBytes.Buffer.String() + if enc != encBytes { + i := 0 + for i < len(enc) && i < len(encBytes) && enc[i] == encBytes[i] { + i++ + } + enc = enc[i:] + encBytes = encBytes[i:] + i = 0 + for i < len(enc) && i < len(encBytes) && enc[len(enc)-i-1] == encBytes[len(encBytes)-i-1] { + i++ + } + enc = enc[:len(enc)-i] + encBytes = encBytes[:len(encBytes)-i] + + if len(enc) > 20 { + enc = enc[:20] + "..." + } + if len(encBytes) > 20 { + encBytes = encBytes[:20] + "..." + } + + t.Errorf("with escapeHTML=%t, encodings differ at %#q vs %#q", + escapeHTML, enc, encBytes) + } + } +} + +func TestIssue10281(t *testing.T) { + type Foo struct { + N Number + } + x := Foo{Number(`invalid`)} + + b, err := Marshal(&x) + if err == nil { + t.Errorf("Marshal(&x) = %#q; want error", b) + } +} + +func TestHTMLEscape(t *testing.T) { + var b, want bytes.Buffer + m := `{"M":"foo &` + "\xe2\x80\xa8 \xe2\x80\xa9" + `"}` + want.Write([]byte(`{"M":"\u003chtml\u003efoo \u0026\u2028 \u2029\u003c/html\u003e"}`)) + HTMLEscape(&b, []byte(m)) + if !bytes.Equal(b.Bytes(), want.Bytes()) { + t.Errorf("HTMLEscape(&b, []byte(m)) = %s; want %s", b.Bytes(), want.Bytes()) + } +} + +// golang.org/issue/8582 +func TestEncodePointerString(t *testing.T) { + type stringPointer struct { + N *int64 `json:"n,string"` + } + var n int64 = 42 + b, err := Marshal(stringPointer{N: &n}) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + if got, want := string(b), `{"n":"42"}`; got != want { + t.Errorf("Marshal = %s, want %s", got, want) + } + var back stringPointer + err = Unmarshal(b, &back) + if err != nil { + t.Fatalf("Unmarshal: %v", err) + } + if back.N == nil { + t.Fatalf("Unmarshaled nil N field") + } + if *back.N != 42 { + t.Fatalf("*N = %d; want 42", *back.N) + } +} + +var encodeStringTests = []struct { + in string + out string +}{ + {"\x00", `"\u0000"`}, + {"\x01", `"\u0001"`}, + {"\x02", `"\u0002"`}, + {"\x03", `"\u0003"`}, + {"\x04", `"\u0004"`}, + {"\x05", `"\u0005"`}, + {"\x06", `"\u0006"`}, + {"\x07", `"\u0007"`}, + {"\x08", `"\u0008"`}, + {"\x09", `"\t"`}, + {"\x0a", `"\n"`}, + {"\x0b", `"\u000b"`}, + {"\x0c", `"\u000c"`}, + {"\x0d", `"\r"`}, + {"\x0e", `"\u000e"`}, + {"\x0f", `"\u000f"`}, + {"\x10", `"\u0010"`}, + {"\x11", `"\u0011"`}, + {"\x12", `"\u0012"`}, + {"\x13", `"\u0013"`}, + {"\x14", `"\u0014"`}, + {"\x15", `"\u0015"`}, + {"\x16", `"\u0016"`}, + {"\x17", `"\u0017"`}, + {"\x18", `"\u0018"`}, + {"\x19", `"\u0019"`}, + {"\x1a", `"\u001a"`}, + {"\x1b", `"\u001b"`}, + {"\x1c", `"\u001c"`}, + {"\x1d", `"\u001d"`}, + {"\x1e", `"\u001e"`}, + {"\x1f", `"\u001f"`}, +} + +func TestEncodeString(t *testing.T) { + for _, tt := range encodeStringTests { + b, err := Marshal(tt.in) + if err != nil { + t.Errorf("Marshal(%q): %v", tt.in, err) + continue + } + out := string(b) + if out != tt.out { + t.Errorf("Marshal(%q) = %#q, want %#q", tt.in, out, tt.out) + } + } +} + +type jsonbyte byte + +func (b jsonbyte) MarshalJSON() ([]byte, error) { return tenc(`{"JB":%d}`, b) } + +type textbyte byte + +func (b textbyte) MarshalText() ([]byte, error) { return tenc(`TB:%d`, b) } + +type jsonint int + +func (i jsonint) MarshalJSON() ([]byte, error) { return tenc(`{"JI":%d}`, i) } + +type textint int + +func (i textint) MarshalText() ([]byte, error) { return tenc(`TI:%d`, i) } + +func tenc(format string, a ...interface{}) ([]byte, error) { + var buf bytes.Buffer + fmt.Fprintf(&buf, format, a...) + return buf.Bytes(), nil +} + +// Issue 13783 +func TestEncodeBytekind(t *testing.T) { + testdata := []struct { + data interface{} + want string + }{ + {byte(7), "7"}, + {jsonbyte(7), `{"JB":7}`}, + {textbyte(4), `"TB:4"`}, + {jsonint(5), `{"JI":5}`}, + {textint(1), `"TI:1"`}, + {[]byte{0, 1}, `"AAE="`}, + {[]jsonbyte{0, 1}, `[{"JB":0},{"JB":1}]`}, + {[][]jsonbyte{{0, 1}, {3}}, `[[{"JB":0},{"JB":1}],[{"JB":3}]]`}, + {[]textbyte{2, 3}, `["TB:2","TB:3"]`}, + {[]jsonint{5, 4}, `[{"JI":5},{"JI":4}]`}, + {[]textint{9, 3}, `["TI:9","TI:3"]`}, + {[]int{9, 3}, `[9,3]`}, + } + for _, d := range testdata { + js, err := Marshal(d.data) + if err != nil { + t.Error(err) + continue + } + got, want := string(js), d.want + if got != want { + t.Errorf("got %s, want %s", got, want) + } + } +} + +func TestTextMarshalerMapKeysAreSorted(t *testing.T) { + b, err := Marshal(map[unmarshalerText]int{ + {"x", "y"}: 1, + {"y", "x"}: 2, + {"a", "z"}: 3, + {"z", "a"}: 4, + }) + if err != nil { + t.Fatalf("Failed to Marshal text.Marshaler: %v", err) + } + const want = `{"a:z":3,"x:y":1,"y:x":2,"z:a":4}` + if string(b) != want { + t.Errorf("Marshal map with text.Marshaler keys: got %#q, want %#q", b, want) + } +} + +var re = regexp.MustCompile + +// syntactic checks on form of marshaled floating point numbers. +var badFloatREs = []*regexp.Regexp{ + re(`p`), // no binary exponential notation + re(`^\+`), // no leading + sign + re(`^-?0[^.]`), // no unnecessary leading zeros + re(`^-?\.`), // leading zero required before decimal point + re(`\.(e|$)`), // no trailing decimal + re(`\.[0-9]+0(e|$)`), // no trailing zero in fraction + re(`^-?(0|[0-9]{2,})\..*e`), // exponential notation must have normalized mantissa + re(`e[0-9]`), // positive exponent must be signed + re(`e[+-]0`), // exponent must not have leading zeros + re(`e-[1-6]$`), // not tiny enough for exponential notation + re(`e+(.|1.|20)$`), // not big enough for exponential notation + re(`^-?0\.0000000`), // too tiny, should use exponential notation + re(`^-?[0-9]{22}`), // too big, should use exponential notation + re(`[1-9][0-9]{16}[1-9]`), // too many significant digits in integer + re(`[1-9][0-9.]{17}[1-9]`), // too many significant digits in decimal + // below here for float32 only + re(`[1-9][0-9]{8}[1-9]`), // too many significant digits in integer + re(`[1-9][0-9.]{9}[1-9]`), // too many significant digits in decimal +} + +func TestMarshalFloat(t *testing.T) { + t.Parallel() + nfail := 0 + test := func(f float64, bits int) { + vf := interface{}(f) + if bits == 32 { + f = float64(float32(f)) // round + vf = float32(f) + } + bout, err := Marshal(vf) + if err != nil { + t.Errorf("Marshal(%T(%g)): %v", vf, vf, err) + nfail++ + return + } + out := string(bout) + + // result must convert back to the same float + g, err := strconv.ParseFloat(out, bits) + if err != nil { + t.Errorf("Marshal(%T(%g)) = %q, cannot parse back: %v", vf, vf, out, err) + nfail++ + return + } + if f != g || fmt.Sprint(f) != fmt.Sprint(g) { // fmt.Sprint handles ยฑ0 + t.Errorf("Marshal(%T(%g)) = %q (is %g, not %g)", vf, vf, out, float32(g), vf) + nfail++ + return + } + + bad := badFloatREs + if bits == 64 { + bad = bad[:len(bad)-2] + } + for _, re := range bad { + if re.MatchString(out) { + t.Errorf("Marshal(%T(%g)) = %q, must not match /%s/", vf, vf, out, re) + nfail++ + return + } + } + } + + var ( + bigger = math.Inf(+1) + smaller = math.Inf(-1) + ) + + var digits = "1.2345678901234567890123" + for i := len(digits); i >= 2; i-- { + if testing.Short() && i < len(digits)-4 { + break + } + for exp := -30; exp <= 30; exp++ { + for _, sign := range "+-" { + for bits := 32; bits <= 64; bits += 32 { + s := fmt.Sprintf("%c%se%d", sign, digits[:i], exp) + f, err := strconv.ParseFloat(s, bits) + if err != nil { + log.Fatal(err) + } + next := math.Nextafter + if bits == 32 { + next = func(g, h float64) float64 { + return float64(math.Nextafter32(float32(g), float32(h))) + } + } + test(f, bits) + test(next(f, bigger), bits) + test(next(f, smaller), bits) + if nfail > 50 { + t.Fatalf("stopping test early") + } + } + } + } + } + test(0, 64) + test(math.Copysign(0, -1), 64) + test(0, 32) + test(math.Copysign(0, -1), 32) +} + +func TestMarshalRawMessageValue(t *testing.T) { + type ( + T1 struct { + M RawMessage `json:",omitempty"` + } + T2 struct { + M *RawMessage `json:",omitempty"` + } + ) + + var ( + rawNil = RawMessage(nil) + rawEmpty = RawMessage([]byte{}) + rawText = RawMessage([]byte(`"foo"`)) + ) + + tests := []struct { + in interface{} + want string + ok bool + }{ + // Test with nil RawMessage. + {rawNil, "null", true}, + {&rawNil, "null", true}, + {[]interface{}{rawNil}, "[null]", true}, + {&[]interface{}{rawNil}, "[null]", true}, + {[]interface{}{&rawNil}, "[null]", true}, + {&[]interface{}{&rawNil}, "[null]", true}, + {struct{ M RawMessage }{rawNil}, `{"M":null}`, true}, + {&struct{ M RawMessage }{rawNil}, `{"M":null}`, true}, + {struct{ M *RawMessage }{&rawNil}, `{"M":null}`, true}, + {&struct{ M *RawMessage }{&rawNil}, `{"M":null}`, true}, + {map[string]interface{}{"M": rawNil}, `{"M":null}`, true}, + {&map[string]interface{}{"M": rawNil}, `{"M":null}`, true}, + {map[string]interface{}{"M": &rawNil}, `{"M":null}`, true}, + {&map[string]interface{}{"M": &rawNil}, `{"M":null}`, true}, + {T1{rawNil}, "{}", true}, + {T2{&rawNil}, `{"M":null}`, true}, + {&T1{rawNil}, "{}", true}, + {&T2{&rawNil}, `{"M":null}`, true}, + + // Test with empty, but non-nil, RawMessage. + {rawEmpty, "", false}, + {&rawEmpty, "", false}, + {[]interface{}{rawEmpty}, "", false}, + {&[]interface{}{rawEmpty}, "", false}, + {[]interface{}{&rawEmpty}, "", false}, + {&[]interface{}{&rawEmpty}, "", false}, + {struct{ X RawMessage }{rawEmpty}, "", false}, + {&struct{ X RawMessage }{rawEmpty}, "", false}, + {struct{ X *RawMessage }{&rawEmpty}, "", false}, + {&struct{ X *RawMessage }{&rawEmpty}, "", false}, + {map[string]interface{}{"nil": rawEmpty}, "", false}, + {&map[string]interface{}{"nil": rawEmpty}, "", false}, + {map[string]interface{}{"nil": &rawEmpty}, "", false}, + {&map[string]interface{}{"nil": &rawEmpty}, "", false}, + {T1{rawEmpty}, "{}", true}, + {T2{&rawEmpty}, "", false}, + {&T1{rawEmpty}, "{}", true}, + {&T2{&rawEmpty}, "", false}, + + // Test with RawMessage with some text. + // + // The tests below marked with Issue6458 used to generate "ImZvbyI=" instead "foo". + // This behavior was intentionally changed in Go 1.8. + // See https://golang.org/issues/14493#issuecomment-255857318 + {rawText, `"foo"`, true}, // Issue6458 + {&rawText, `"foo"`, true}, + {[]interface{}{rawText}, `["foo"]`, true}, // Issue6458 + {&[]interface{}{rawText}, `["foo"]`, true}, // Issue6458 + {[]interface{}{&rawText}, `["foo"]`, true}, + {&[]interface{}{&rawText}, `["foo"]`, true}, + {struct{ M RawMessage }{rawText}, `{"M":"foo"}`, true}, // Issue6458 + {&struct{ M RawMessage }{rawText}, `{"M":"foo"}`, true}, + {struct{ M *RawMessage }{&rawText}, `{"M":"foo"}`, true}, + {&struct{ M *RawMessage }{&rawText}, `{"M":"foo"}`, true}, + {map[string]interface{}{"M": rawText}, `{"M":"foo"}`, true}, // Issue6458 + {&map[string]interface{}{"M": rawText}, `{"M":"foo"}`, true}, // Issue6458 + {map[string]interface{}{"M": &rawText}, `{"M":"foo"}`, true}, + {&map[string]interface{}{"M": &rawText}, `{"M":"foo"}`, true}, + {T1{rawText}, `{"M":"foo"}`, true}, // Issue6458 + {T2{&rawText}, `{"M":"foo"}`, true}, + {&T1{rawText}, `{"M":"foo"}`, true}, + {&T2{&rawText}, `{"M":"foo"}`, true}, + } + + for i, tt := range tests { + b, err := Marshal(tt.in) + if ok := (err == nil); ok != tt.ok { + if err != nil { + t.Errorf("test %d, unexpected failure: %v", i, err) + } else { + t.Errorf("test %d, unexpected success", i) + } + } + if got := string(b); got != tt.want { + t.Errorf("test %d, Marshal(%#v) = %q, want %q", i, tt.in, got, tt.want) + } + } +} + +type marshalPanic struct{} + +func (marshalPanic) MarshalJSON() ([]byte, error) { panic(0xdead) } + +func TestMarshalPanic(t *testing.T) { + defer func() { + if got := recover(); !reflect.DeepEqual(got, 0xdead) { + t.Errorf("panic() = (%T)(%v), want 0xdead", got, got) + } + }() + Marshal(&marshalPanic{}) + t.Error("Marshal should have panicked") +} + +func TestMarshalUncommonFieldNames(t *testing.T) { + v := struct { + A0, ร€, Aฮฒ int + }{} + b, err := Marshal(v) + if err != nil { + t.Fatal("Marshal:", err) + } + want := `{"A0":0,"ร€":0,"Aฮฒ":0}` + got := string(b) + if got != want { + t.Fatalf("Marshal: got %s want %s", got, want) + } +} diff --git a/utils/encoding/json/golang_example_marshaling_test.go b/utils/encoding/json/golang_example_marshaling_test.go new file mode 100644 index 00000000..7f15c742 --- /dev/null +++ b/utils/encoding/json/golang_example_marshaling_test.go @@ -0,0 +1,73 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package json_test + +import ( + "encoding/json" + "fmt" + "log" + "strings" +) + +type Animal int + +const ( + Unknown Animal = iota + Gopher + Zebra +) + +func (a *Animal) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + switch strings.ToLower(s) { + default: + *a = Unknown + case "gopher": + *a = Gopher + case "zebra": + *a = Zebra + } + + return nil +} + +func (a Animal) MarshalJSON() ([]byte, error) { + var s string + switch a { + default: + s = "unknown" + case Gopher: + s = "gopher" + case Zebra: + s = "zebra" + } + + return json.Marshal(s) +} + +func Example_customMarshalJSON() { + blob := `["gopher","armadillo","zebra","unknown","gopher","bee","gopher","zebra"]` + var zoo []Animal + if err := json.Unmarshal([]byte(blob), &zoo); err != nil { + log.Fatal(err) + } + + census := make(map[Animal]int) + for _, animal := range zoo { + census[animal] += 1 + } + + fmt.Printf("Zoo Census:\n* Gophers: %d\n* Zebras: %d\n* Unknown: %d\n", + census[Gopher], census[Zebra], census[Unknown]) + + // Output: + // Zoo Census: + // * Gophers: 3 + // * Zebras: 2 + // * Unknown: 3 +} diff --git a/utils/encoding/json/golang_example_test.go b/utils/encoding/json/golang_example_test.go new file mode 100644 index 00000000..2088c342 --- /dev/null +++ b/utils/encoding/json/golang_example_test.go @@ -0,0 +1,310 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package json_test + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "os" + "strings" +) + +func ExampleMarshal() { + type ColorGroup struct { + ID int + Name string + Colors []string + } + group := ColorGroup{ + ID: 1, + Name: "Reds", + Colors: []string{"Crimson", "Red", "Ruby", "Maroon"}, + } + b, err := json.Marshal(group) + if err != nil { + fmt.Println("error:", err) + } + os.Stdout.Write(b) + // Output: + // {"ID":1,"Name":"Reds","Colors":["Crimson","Red","Ruby","Maroon"]} +} + +func ExampleUnmarshal() { + var jsonBlob = []byte(`[ + {"Name": "Platypus", "Order": "Monotremata"}, + {"Name": "Quoll", "Order": "Dasyuromorphia"} +]`) + type Animal struct { + Name string + Order string + } + var animals []Animal + err := json.Unmarshal(jsonBlob, &animals) + if err != nil { + fmt.Println("error:", err) + } + fmt.Printf("%+v", animals) + // Output: + // [{Name:Platypus Order:Monotremata} {Name:Quoll Order:Dasyuromorphia}] +} + +// This example uses a Decoder to decode a stream of distinct JSON values. +func ExampleDecoder() { + const jsonStream = ` + {"Name": "Ed", "Text": "Knock knock."} + {"Name": "Sam", "Text": "Who's there?"} + {"Name": "Ed", "Text": "Go fmt."} + {"Name": "Sam", "Text": "Go fmt who?"} + {"Name": "Ed", "Text": "Go fmt yourself!"} +` + type Message struct { + Name, Text string + } + dec := json.NewDecoder(strings.NewReader(jsonStream)) + for { + var m Message + if err := dec.Decode(&m); err == io.EOF { + break + } else if err != nil { + log.Fatal(err) + } + fmt.Printf("%s: %s\n", m.Name, m.Text) + } + // Output: + // Ed: Knock knock. + // Sam: Who's there? + // Ed: Go fmt. + // Sam: Go fmt who? + // Ed: Go fmt yourself! +} + +// This example uses a Decoder to decode a stream of distinct JSON values. +func ExampleDecoder_Token() { + const jsonStream = ` + {"Message": "Hello", "Array": [1, 2, 3], "Null": null, "Number": 1.234} +` + dec := json.NewDecoder(strings.NewReader(jsonStream)) + for { + t, err := dec.Token() + if err == io.EOF { + break + } + if err != nil { + log.Fatal(err) + } + fmt.Printf("%T: %v", t, t) + if dec.More() { + fmt.Printf(" (more)") + } + fmt.Printf("\n") + } + // Output: + // json.Delim: { (more) + // string: Message (more) + // string: Hello (more) + // string: Array (more) + // json.Delim: [ (more) + // float64: 1 (more) + // float64: 2 (more) + // float64: 3 + // json.Delim: ] (more) + // string: Null (more) + // : (more) + // string: Number (more) + // float64: 1.234 + // json.Delim: } +} + +// This example uses a Decoder to decode a streaming array of JSON objects. +func ExampleDecoder_Decode_stream() { + const jsonStream = ` + [ + {"Name": "Ed", "Text": "Knock knock."}, + {"Name": "Sam", "Text": "Who's there?"}, + {"Name": "Ed", "Text": "Go fmt."}, + {"Name": "Sam", "Text": "Go fmt who?"}, + {"Name": "Ed", "Text": "Go fmt yourself!"} + ] +` + type Message struct { + Name, Text string + } + dec := json.NewDecoder(strings.NewReader(jsonStream)) + + // read open bracket + t, err := dec.Token() + if err != nil { + log.Fatal(err) + } + fmt.Printf("%T: %v\n", t, t) + + // while the array contains values + for dec.More() { + var m Message + // decode an array value (Message) + err := dec.Decode(&m) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("%v: %v\n", m.Name, m.Text) + } + + // read closing bracket + t, err = dec.Token() + if err != nil { + log.Fatal(err) + } + fmt.Printf("%T: %v\n", t, t) + + // Output: + // json.Delim: [ + // Ed: Knock knock. + // Sam: Who's there? + // Ed: Go fmt. + // Sam: Go fmt who? + // Ed: Go fmt yourself! + // json.Delim: ] +} + +// This example uses RawMessage to delay parsing part of a JSON message. +func ExampleRawMessage_unmarshal() { + type Color struct { + Space string + Point json.RawMessage // delay parsing until we know the color space + } + type RGB struct { + R uint8 + G uint8 + B uint8 + } + type YCbCr struct { + Y uint8 + Cb int8 + Cr int8 + } + + var j = []byte(`[ + {"Space": "YCbCr", "Point": {"Y": 255, "Cb": 0, "Cr": -10}}, + {"Space": "RGB", "Point": {"R": 98, "G": 218, "B": 255}} +]`) + var colors []Color + err := json.Unmarshal(j, &colors) + if err != nil { + log.Fatalln("error:", err) + } + + for _, c := range colors { + var dst interface{} + switch c.Space { + case "RGB": + dst = new(RGB) + case "YCbCr": + dst = new(YCbCr) + } + err := json.Unmarshal(c.Point, dst) + if err != nil { + log.Fatalln("error:", err) + } + fmt.Println(c.Space, dst) + } + // Output: + // YCbCr &{255 0 -10} + // RGB &{98 218 255} +} + +// This example uses RawMessage to use a precomputed JSON during marshal. +func ExampleRawMessage_marshal() { + h := json.RawMessage(`{"precomputed": true}`) + + c := struct { + Header *json.RawMessage `json:"header"` + Body string `json:"body"` + }{Header: &h, Body: "Hello Gophers!"} + + b, err := json.MarshalIndent(&c, "", "\t") + if err != nil { + fmt.Println("error:", err) + } + os.Stdout.Write(b) + + // Output: + // { + // "header": { + // "precomputed": true + // }, + // "body": "Hello Gophers!" + // } +} + +func ExampleIndent() { + type Road struct { + Name string + Number int + } + roads := []Road{ + {"Diamond Fork", 29}, + {"Sheep Creek", 51}, + } + + b, err := json.Marshal(roads) + if err != nil { + log.Fatal(err) + } + + var out bytes.Buffer + json.Indent(&out, b, "=", "\t") + out.WriteTo(os.Stdout) + // Output: + // [ + // = { + // = "Name": "Diamond Fork", + // = "Number": 29 + // = }, + // = { + // = "Name": "Sheep Creek", + // = "Number": 51 + // = } + // =] +} + +func ExampleMarshalIndent() { + data := map[string]int{ + "a": 1, + "b": 2, + } + + json, err := json.MarshalIndent(data, "", "") + if err != nil { + log.Fatal(err) + } + + fmt.Println(string(json)) + // Output: + // { + // "a": 1, + // "b": 2 + // } +} + +func ExampleValid() { + goodJSON := `{"example": 1}` + badJSON := `{"example":2:]}}` + + fmt.Println(json.Valid([]byte(goodJSON)), json.Valid([]byte(badJSON))) + // Output: + // true false +} + +func ExampleHTMLEscape() { + var out bytes.Buffer + json.HTMLEscape(&out, []byte(`{"Name":"HTML content"}`)) + out.WriteTo(os.Stdout) + // Output: + //{"Name":"\u003cb\u003eHTML content\u003c/b\u003e"} +} diff --git a/utils/encoding/json/golang_number_test.go b/utils/encoding/json/golang_number_test.go new file mode 100644 index 00000000..f89ac8b3 --- /dev/null +++ b/utils/encoding/json/golang_number_test.go @@ -0,0 +1,129 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package json + +import ( + "regexp" + "testing" +) + +func TestNumberIsValid(t *testing.T) { + // From: https://stackoverflow.com/a/13340826 + var jsonNumberRegexp = regexp.MustCompile(`^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$`) + + validTests := []string{ + "0", + "-0", + "1", + "-1", + "0.1", + "-0.1", + "1234", + "-1234", + "12.34", + "-12.34", + "12E0", + "12E1", + "12e34", + "12E-0", + "12e+1", + "12e-34", + "-12E0", + "-12E1", + "-12e34", + "-12E-0", + "-12e+1", + "-12e-34", + "1.2E0", + "1.2E1", + "1.2e34", + "1.2E-0", + "1.2e+1", + "1.2e-34", + "-1.2E0", + "-1.2E1", + "-1.2e34", + "-1.2E-0", + "-1.2e+1", + "-1.2e-34", + "0E0", + "0E1", + "0e34", + "0E-0", + "0e+1", + "0e-34", + "-0E0", + "-0E1", + "-0e34", + "-0E-0", + "-0e+1", + "-0e-34", + } + + for _, test := range validTests { + if !isValidNumber(test) { + t.Errorf("%s should be valid", test) + } + + var f float64 + if err := Unmarshal([]byte(test), &f); err != nil { + t.Errorf("%s should be valid but Unmarshal failed: %v", test, err) + } + + if !jsonNumberRegexp.MatchString(test) { + t.Errorf("%s should be valid but regexp does not match", test) + } + } + + invalidTests := []string{ + "", + "invalid", + "1.0.1", + "1..1", + "-1-2", + "012a42", + "01.2", + "012", + "12E12.12", + "1e2e3", + "1e+-2", + "1e--23", + "1e", + "e1", + "1e+", + "1ea", + "1a", + "1.a", + "1.", + "01", + "1.e1", + } + + for _, test := range invalidTests { + var f float64 + if err := Unmarshal([]byte(test), &f); err == nil { + t.Errorf("%s should be invalid but unmarshal wrote %v", test, f) + } + + if jsonNumberRegexp.MatchString(test) { + t.Errorf("%s should be invalid but matches regexp", test) + } + } +} + +func BenchmarkNumberIsValid(b *testing.B) { + s := "-61657.61667E+61673" + for i := 0; i < b.N; i++ { + isValidNumber(s) + } +} + +func BenchmarkNumberIsValidRegexp(b *testing.B) { + var jsonNumberRegexp = regexp.MustCompile(`^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$`) + s := "-61657.61667E+61673" + for i := 0; i < b.N; i++ { + jsonNumberRegexp.MatchString(s) + } +} diff --git a/utils/encoding/json/golang_scanner_test.go b/utils/encoding/json/golang_scanner_test.go new file mode 100644 index 00000000..a26bacee --- /dev/null +++ b/utils/encoding/json/golang_scanner_test.go @@ -0,0 +1,296 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package json + +import ( + "bytes" + "math" + "math/rand" + "testing" +) + +var validTests = []struct { + data string + ok bool +}{ + {`foo`, false}, + {`}{`, false}, + {`{]`, false}, + {`{}`, true}, + {`{"foo":"bar"}`, true}, + {`{"foo":"bar","bar":{"baz":["qux"]}}`, true}, +} + +func TestValid(t *testing.T) { + for _, tt := range validTests { + if ok := Valid([]byte(tt.data)); ok != tt.ok { + t.Errorf("Valid(%#q) = %v, want %v", tt.data, ok, tt.ok) + } + } +} + +// Tests of simple examples. + +type example struct { + compact string + indent string +} + +var examples = []example{ + {`1`, `1`}, + {`{}`, `{}`}, + {`[]`, `[]`}, + {`{"":2}`, "{\n\t\"\": 2\n}"}, + {`[3]`, "[\n\t3\n]"}, + {`[1,2,3]`, "[\n\t1,\n\t2,\n\t3\n]"}, + {`{"x":1}`, "{\n\t\"x\": 1\n}"}, + {ex1, ex1i}, + {"{\"\":\"<>&\u2028\u2029\"}", "{\n\t\"\": \"<>&\u2028\u2029\"\n}"}, // See golang.org/issue/34070 +} + +var ex1 = `[true,false,null,"x",1,1.5,0,-5e+2]` + +var ex1i = `[ + true, + false, + null, + "x", + 1, + 1.5, + 0, + -5e+2 +]` + +func TestCompact(t *testing.T) { + var buf bytes.Buffer + for _, tt := range examples { + buf.Reset() + if err := Compact(&buf, []byte(tt.compact)); err != nil { + t.Errorf("Compact(%#q): %v", tt.compact, err) + } else if s := buf.String(); s != tt.compact { + t.Errorf("Compact(%#q) = %#q, want original", tt.compact, s) + } + + buf.Reset() + if err := Compact(&buf, []byte(tt.indent)); err != nil { + t.Errorf("Compact(%#q): %v", tt.indent, err) + continue + } else if s := buf.String(); s != tt.compact { + t.Errorf("Compact(%#q) = %#q, want %#q", tt.indent, s, tt.compact) + } + } +} + +func TestCompactSeparators(t *testing.T) { + // U+2028 and U+2029 should be escaped inside strings. + // They should not appear outside strings. + tests := []struct { + in, compact string + }{ + {"{\"\u2028\": 1}", "{\"\u2028\":1}"}, + {"{\"\u2029\" :2}", "{\"\u2029\":2}"}, + } + for _, tt := range tests { + var buf bytes.Buffer + if err := Compact(&buf, []byte(tt.in)); err != nil { + t.Errorf("Compact(%q): %v", tt.in, err) + } else if s := buf.String(); s != tt.compact { + t.Errorf("Compact(%q) = %q, want %q", tt.in, s, tt.compact) + } + } +} + +func TestIndent(t *testing.T) { + var buf bytes.Buffer + for _, tt := range examples { + buf.Reset() + if err := Indent(&buf, []byte(tt.indent), "", "\t"); err != nil { + t.Errorf("Indent(%#q): %v", tt.indent, err) + } else if s := buf.String(); s != tt.indent { + t.Errorf("Indent(%#q) = %#q, want original", tt.indent, s) + } + + buf.Reset() + if err := Indent(&buf, []byte(tt.compact), "", "\t"); err != nil { + t.Errorf("Indent(%#q): %v", tt.compact, err) + continue + } else if s := buf.String(); s != tt.indent { + t.Errorf("Indent(%#q) = %#q, want %#q", tt.compact, s, tt.indent) + } + } +} + +// Tests of a large random structure. + +func TestCompactBig(t *testing.T) { + initBig() + var buf bytes.Buffer + if err := Compact(&buf, jsonBig); err != nil { + t.Fatalf("Compact: %v", err) + } + b := buf.Bytes() + if !bytes.Equal(b, jsonBig) { + t.Error("Compact(jsonBig) != jsonBig") + diff(t, b, jsonBig) + return + } +} + +func TestIndentBig(t *testing.T) { + t.Parallel() + initBig() + var buf bytes.Buffer + if err := Indent(&buf, jsonBig, "", "\t"); err != nil { + t.Fatalf("Indent1: %v", err) + } + b := buf.Bytes() + if len(b) == len(jsonBig) { + // jsonBig is compact (no unnecessary spaces); + // indenting should make it bigger + t.Fatalf("Indent(jsonBig) did not get bigger") + } + + // should be idempotent + var buf1 bytes.Buffer + if err := Indent(&buf1, b, "", "\t"); err != nil { + t.Fatalf("Indent2: %v", err) + } + b1 := buf1.Bytes() + if !bytes.Equal(b1, b) { + t.Error("Indent(Indent(jsonBig)) != Indent(jsonBig)") + diff(t, b1, b) + return + } + + // should get back to original + buf1.Reset() + if err := Compact(&buf1, b); err != nil { + t.Fatalf("Compact: %v", err) + } + b1 = buf1.Bytes() + if !bytes.Equal(b1, jsonBig) { + t.Error("Compact(Indent(jsonBig)) != jsonBig") + diff(t, b1, jsonBig) + return + } +} + +type indentErrorTest struct { + in string + err error +} + +var indentErrorTests = []indentErrorTest{ + {`{"X": "foo", "Y"}`, &testSyntaxError{"invalid character '}' after object key", 17}}, + {`{"X": "foo" "Y": "bar"}`, &testSyntaxError{"invalid character '\"' after object key:value pair", 13}}, +} + +func TestIndentErrors(t *testing.T) { + for i, tt := range indentErrorTests { + slice := make([]uint8, 0) + buf := bytes.NewBuffer(slice) + err := Indent(buf, []uint8(tt.in), "", "") + assertErrorPresence(t, tt.err, err, i) + } +} + +func diff(t *testing.T, a, b []byte) { + for i := 0; ; i++ { + if i >= len(a) || i >= len(b) || a[i] != b[i] { + j := i - 10 + if j < 0 { + j = 0 + } + t.Errorf("diverge at %d: ยซ%sยป vs ยซ%sยป", i, trim(a[j:]), trim(b[j:])) + return + } + } +} + +func trim(b []byte) []byte { + if len(b) > 20 { + return b[0:20] + } + return b +} + +// Generate a random JSON object. + +var jsonBig []byte + +func initBig() { + n := 10000 + if testing.Short() { + n = 100 + } + b, err := Marshal(genValue(n)) + if err != nil { + panic(err) + } + jsonBig = b +} + +func genValue(n int) interface{} { + if n > 1 { + switch rand.Intn(2) { + case 0: + return genArray(n) + case 1: + return genMap(n) + } + } + switch rand.Intn(3) { + case 0: + return rand.Intn(2) == 0 + case 1: + return rand.NormFloat64() + case 2: + return genString(30) + } + panic("unreachable") +} + +func genString(stddev float64) string { + n := int(math.Abs(rand.NormFloat64()*stddev + stddev/2)) + c := make([]rune, n) + for i := range c { + f := math.Abs(rand.NormFloat64()*64 + 32) + if f > 0x10ffff { + f = 0x10ffff + } + c[i] = rune(f) + } + return string(c) +} + +func genArray(n int) []interface{} { + f := int(math.Abs(rand.NormFloat64()) * math.Min(10, float64(n/2))) + if f > n { + f = n + } + if f < 1 { + f = 1 + } + x := make([]interface{}, f) + for i := range x { + x[i] = genValue(((i+1)*n)/f - (i*n)/f) + } + return x +} + +func genMap(n int) map[string]interface{} { + f := int(math.Abs(rand.NormFloat64()) * math.Min(10, float64(n/2))) + if f > n { + f = n + } + if n > 0 && f == 0 { + f = 1 + } + x := make(map[string]interface{}) + for i := 0; i < f; i++ { + x[genString(10)] = genValue(((i+1)*n)/f - (i*n)/f) + } + return x +} diff --git a/utils/encoding/json/golang_shim_test.go b/utils/encoding/json/golang_shim_test.go new file mode 100644 index 00000000..b615e360 --- /dev/null +++ b/utils/encoding/json/golang_shim_test.go @@ -0,0 +1,72 @@ +// This file is a shim for dependencies of golang_*_test.go files that are normally provided by the standard library. +// It helps importing those files with minimal changes. +package json + +import ( + "bytes" + "reflect" + "sync" + "testing" +) + +// Field cache used in golang_bench_test.go +var fieldCache = sync.Map{} + +func cachedTypeFields(reflect.Type) {} + +// Fake test env for golang_bench_test.go +type testenvShim struct { +} + +func (ts testenvShim) Builder() string { + return "" +} + +var testenv testenvShim + +// Fake scanner for golang_decode_test.go +type scanner struct { +} + +func checkValid(in []byte, scan *scanner) error { + return nil +} + +// Actual isSpace implementation +func isSpace(c byte) bool { + return c == ' ' || c == '\t' || c == '\r' || c == '\n' +} + +// Fake encoder for golang_encode_test.go +type encodeState struct { + Buffer bytes.Buffer +} + +func (es *encodeState) string(s string, escapeHTML bool) { +} + +func (es *encodeState) stringBytes(b []byte, escapeHTML bool) { +} + +// Fake number test +func isValidNumber(n string) bool { + return true +} + +func assertErrorPresence(t *testing.T, expected error, actual error, prefixes ...interface{}) { + if expected != nil && actual == nil { + errorWithPrefixes(t, prefixes, "expected error, but did not get an error") + } else if expected == nil && actual != nil { + errorWithPrefixes(t, prefixes, "did not expect error but got %v", actual) + } +} + +func errorWithPrefixes(t *testing.T, prefixes []interface{}, format string, elements ...interface{}) { + fullFormat := format + allElements := append(prefixes, elements...) + + for range prefixes { + fullFormat = "%v: " + fullFormat + } + t.Errorf(fullFormat, allElements...) +} diff --git a/utils/encoding/json/golang_tagkey_test.go b/utils/encoding/json/golang_tagkey_test.go new file mode 100644 index 00000000..f77c49c7 --- /dev/null +++ b/utils/encoding/json/golang_tagkey_test.go @@ -0,0 +1,120 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package json + +import ( + "testing" +) + +type basicLatin2xTag struct { + V string `json:"$%-/"` +} + +type basicLatin3xTag struct { + V string `json:"0123456789"` +} + +type basicLatin4xTag struct { + V string `json:"ABCDEFGHIJKLMO"` +} + +type basicLatin5xTag struct { + V string `json:"PQRSTUVWXYZ_"` +} + +type basicLatin6xTag struct { + V string `json:"abcdefghijklmno"` +} + +type basicLatin7xTag struct { + V string `json:"pqrstuvwxyz"` +} + +type miscPlaneTag struct { + V string `json:"่‰ฒใฏๅŒ‚ใธใฉ"` +} + +type percentSlashTag struct { + V string `json:"text/html%"` // https://golang.org/issue/2718 +} + +type punctuationTag struct { + V string `json:"!#$%&()*+-./:<=>?@[]^_{|}~"` // https://golang.org/issue/3546 +} + +type dashTag struct { + V string `json:"-,"` +} + +type emptyTag struct { + W string +} + +type misnamedTag struct { + X string `jsom:"Misnamed"` +} + +type badFormatTag struct { + Y string `:"BadFormat"` +} + +type badCodeTag struct { + Z string `json:" !\"#&'()*+,."` +} + +type spaceTag struct { + Q string `json:"With space"` +} + +type unicodeTag struct { + W string `json:"ฮ•ฮปฮปฮฌฮดฮฑ"` +} + +var structTagObjectKeyTests = []struct { + raw interface{} + value string + key string +}{ + {basicLatin2xTag{"2x"}, "2x", "$%-/"}, + {basicLatin3xTag{"3x"}, "3x", "0123456789"}, + {basicLatin4xTag{"4x"}, "4x", "ABCDEFGHIJKLMO"}, + {basicLatin5xTag{"5x"}, "5x", "PQRSTUVWXYZ_"}, + {basicLatin6xTag{"6x"}, "6x", "abcdefghijklmno"}, + {basicLatin7xTag{"7x"}, "7x", "pqrstuvwxyz"}, + {miscPlaneTag{"ใ„ใ‚ใฏใซใปใธใจ"}, "ใ„ใ‚ใฏใซใปใธใจ", "่‰ฒใฏๅŒ‚ใธใฉ"}, + {dashTag{"foo"}, "foo", "-"}, + {emptyTag{"Pour Moi"}, "Pour Moi", "W"}, + {misnamedTag{"Animal Kingdom"}, "Animal Kingdom", "X"}, + {badFormatTag{"Orfevre"}, "Orfevre", "Y"}, + {badCodeTag{"Reliable Man"}, "Reliable Man", "Z"}, + {percentSlashTag{"brut"}, "brut", "text/html%"}, + {punctuationTag{"Union Rags"}, "Union Rags", "!#$%&()*+-./:<=>?@[]^_{|}~"}, + {spaceTag{"Perreddu"}, "Perreddu", "With space"}, + {unicodeTag{"Loukanikos"}, "Loukanikos", "ฮ•ฮปฮปฮฌฮดฮฑ"}, +} + +func TestStructTagObjectKey(t *testing.T) { + for _, tt := range structTagObjectKeyTests { + b, err := Marshal(tt.raw) + if err != nil { + t.Fatalf("Marshal(%#q) failed: %v", tt.raw, err) + } + var f interface{} + err = Unmarshal(b, &f) + if err != nil { + t.Fatalf("Unmarshal(%#q) failed: %v", b, err) + } + for i, v := range f.(map[string]interface{}) { + switch i { + case tt.key: + if s, ok := v.(string); !ok || s != tt.value { + t.Fatalf("Unexpected value: %#q, want %v", s, tt.value) + } + default: + t.Fatalf("Unexpected key: %#q, from %#q", i, b) + } + } + } +} diff --git a/utils/encoding/json/json.go b/utils/encoding/json/json.go new file mode 100644 index 00000000..60e33666 --- /dev/null +++ b/utils/encoding/json/json.go @@ -0,0 +1,462 @@ +package json + +import ( + "bytes" + "encoding/json" + "io" + "reflect" + "runtime" + "sync" + "unsafe" +) + +// Delim is documented at https://golang.org/pkg/encoding/json/#Delim +type Delim = json.Delim + +// InvalidUTF8Error is documented at https://golang.org/pkg/encoding/json/#InvalidUTF8Error +type InvalidUTF8Error = json.InvalidUTF8Error + +// InvalidUnmarshalError is documented at https://golang.org/pkg/encoding/json/#InvalidUnmarshalError +type InvalidUnmarshalError = json.InvalidUnmarshalError + +// Marshaler is documented at https://golang.org/pkg/encoding/json/#Marshaler +type Marshaler = json.Marshaler + +// MarshalerError is documented at https://golang.org/pkg/encoding/json/#MarshalerError +type MarshalerError = json.MarshalerError + +// Number is documented at https://golang.org/pkg/encoding/json/#Number +type Number = json.Number + +// RawMessage is documented at https://golang.org/pkg/encoding/json/#RawMessage +type RawMessage = json.RawMessage + +// A SyntaxError is a description of a JSON syntax error. +type SyntaxError = json.SyntaxError + +// Token is documented at https://golang.org/pkg/encoding/json/#Token +type Token = json.Token + +// UnmarshalFieldError is documented at https://golang.org/pkg/encoding/json/#UnmarshalFieldError +type UnmarshalFieldError = json.UnmarshalFieldError + +// UnmarshalTypeError is documented at https://golang.org/pkg/encoding/json/#UnmarshalTypeError +type UnmarshalTypeError = json.UnmarshalTypeError + +// Unmarshaler is documented at https://golang.org/pkg/encoding/json/#Unmarshaler +type Unmarshaler = json.Unmarshaler + +// UnsupportedTypeError is documented at https://golang.org/pkg/encoding/json/#UnsupportedTypeError +type UnsupportedTypeError = json.UnsupportedTypeError + +// UnsupportedValueError is documented at https://golang.org/pkg/encoding/json/#UnsupportedValueError +type UnsupportedValueError = json.UnsupportedValueError + +// AppendFlags is a type used to represent configuration options that can be +// applied when formatting json output. +type AppendFlags int + +const ( + // EscapeHTML is a formatting flag used to to escape HTML in json strings. + EscapeHTML AppendFlags = 1 << iota + + // SortMapKeys is formatting flag used to enable sorting of map keys when + // encoding JSON (this matches the behavior of the standard encoding/json + // package). + SortMapKeys + + // TrustRawMessage is a performance optimization flag to skip value + // checking of raw messages. It should only be used if the values are + // known to be valid json (e.g., they were created by json.Unmarshal). + TrustRawMessage +) + +// ParseFlags is a type used to represent configuration options that can be +// applied when parsing json input. +type ParseFlags int + +const ( + // DisallowUnknownFields is a parsing flag used to prevent decoding of + // objects to Go struct values when a field of the input does not match + // with any of the struct fields. + DisallowUnknownFields ParseFlags = 1 << iota + + // UseNumber is a parsing flag used to load numeric values as Number + // instead of float64. + UseNumber + + // DontCopyString is a parsing flag used to provide zero-copy support when + // loading string values from a json payload. It is not always possible to + // avoid dynamic memory allocations, for example when a string is escaped in + // the json data a new buffer has to be allocated, but when the `wire` value + // can be used as content of a Go value the decoder will simply point into + // the input buffer. + DontCopyString + + // DontCopyNumber is a parsing flag used to provide zero-copy support when + // loading Number values (see DontCopyString and DontCopyRawMessage). + DontCopyNumber + + // DontCopyRawMessage is a parsing flag used to provide zero-copy support + // when loading RawMessage values from a json payload. When used, the + // RawMessage values will not be allocated into new memory buffers and + // will instead point directly to the area of the input buffer where the + // value was found. + DontCopyRawMessage + + // DontMatchCaseInsensitiveStructFields is a parsing flag used to prevent + // matching fields in a case-insensitive way. This can prevent degrading + // performance on case conversions, and can also act as a stricter decoding + // mode. + DontMatchCaseInsensitiveStructFields + + // ZeroCopy is a parsing flag that combines all the copy optimizations + // available in the package. + // + // The zero-copy optimizations are better used in request-handler style + // code where none of the values are retained after the handler returns. + ZeroCopy = DontCopyString | DontCopyNumber | DontCopyRawMessage +) + +// Append acts like Marshal but appends the json representation to b instead of +// always reallocating a new slice. +func Append(b []byte, x interface{}, flags AppendFlags) ([]byte, error) { + if x == nil { + // Special case for nil values because it makes the rest of the code + // simpler to assume that it won't be seeing nil pointers. + return append(b, "null"...), nil + } + + t := reflect.TypeOf(x) + p := (*iface)(unsafe.Pointer(&x)).ptr + + cache := cacheLoad() + c, found := cache[typeid(t)] + + if !found { + c = constructCachedCodec(t, cache) + } + + b, err := c.encode(encoder{flags: flags}, b, p) + runtime.KeepAlive(x) + return b, err +} + +// Compact is documented at https://golang.org/pkg/encoding/json/#Compact +func Compact(dst *bytes.Buffer, src []byte) error { + return json.Compact(dst, src) +} + +// HTMLEscape is documented at https://golang.org/pkg/encoding/json/#HTMLEscape +func HTMLEscape(dst *bytes.Buffer, src []byte) { + json.HTMLEscape(dst, src) +} + +// Indent is documented at https://golang.org/pkg/encoding/json/#Indent +func Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error { + return json.Indent(dst, src, prefix, indent) +} + +// Marshal is documented at https://golang.org/pkg/encoding/json/#Marshal +func Marshal(x interface{}) ([]byte, error) { + var err error + var buf = encoderBufferPool.Get().(*encoderBuffer) + + if buf.data, err = Append(buf.data[:0], x, EscapeHTML|SortMapKeys); err != nil { + return nil, err + } + + b := make([]byte, len(buf.data)) + copy(b, buf.data) + encoderBufferPool.Put(buf) + return b, nil +} + +// MarshalIndent is documented at https://golang.org/pkg/encoding/json/#MarshalIndent +func MarshalIndent(x interface{}, prefix, indent string) ([]byte, error) { + b, err := Marshal(x) + + if err == nil { + tmp := &bytes.Buffer{} + tmp.Grow(2 * len(b)) + + Indent(tmp, b, prefix, indent) + b = tmp.Bytes() + } + + return b, err +} + +// Unmarshal is documented at https://golang.org/pkg/encoding/json/#Unmarshal +func Unmarshal(b []byte, x interface{}) error { + r, err := Parse(b, x, 0) + if len(r) != 0 { + if _, ok := err.(*SyntaxError); !ok { + // The encoding/json package prioritizes reporting errors caused by + // unexpected trailing bytes over other issues; here we emulate this + // behavior by overriding the error. + err = syntaxError(r, "invalid character '%c' after top-level value", r[0]) + } + } + return err +} + +// Parse behaves like Unmarshal but the caller can pass a set of flags to +// configure the parsing behavior. +func Parse(b []byte, x interface{}, flags ParseFlags) ([]byte, error) { + t := reflect.TypeOf(x) + p := (*iface)(unsafe.Pointer(&x)).ptr + + if t == nil || p == nil || t.Kind() != reflect.Ptr { + _, r, err := parseValue(skipSpaces(b)) + r = skipSpaces(r) + if err != nil { + return r, err + } + return r, &InvalidUnmarshalError{Type: t} + } + t = t.Elem() + + cache := cacheLoad() + c, found := cache[typeid(t)] + + if !found { + c = constructCachedCodec(t, cache) + } + + r, err := c.decode(decoder{flags: flags}, skipSpaces(b), p) + return skipSpaces(r), err +} + +// Valid is documented at https://golang.org/pkg/encoding/json/#Valid +func Valid(data []byte) bool { + _, data, err := parseValue(skipSpaces(data)) + if err != nil { + return false + } + return len(skipSpaces(data)) == 0 +} + +// Decoder is documented at https://golang.org/pkg/encoding/json/#Decoder +type Decoder struct { + reader io.Reader + buffer []byte + remain []byte + inputOffset int64 + err error + flags ParseFlags +} + +// NewDecoder is documented at https://golang.org/pkg/encoding/json/#NewDecoder +func NewDecoder(r io.Reader) *Decoder { return &Decoder{reader: r} } + +// Buffered is documented at https://golang.org/pkg/encoding/json/#Decoder.Buffered +func (dec *Decoder) Buffered() io.Reader { + return bytes.NewReader(dec.remain) +} + +// Decode is documented at https://golang.org/pkg/encoding/json/#Decoder.Decode +func (dec *Decoder) Decode(v interface{}) error { + raw, err := dec.readValue() + if err != nil { + return err + } + _, err = Parse(raw, v, dec.flags) + return err +} + +const ( + minBufferSize = 32768 + minReadSize = 4096 +) + +// readValue reads one JSON value from the buffer and returns its raw bytes. It +// is optimized for the "one JSON value per line" case. +func (dec *Decoder) readValue() (v []byte, err error) { + var n int + var r []byte + + for { + if len(dec.remain) != 0 { + v, r, err = parseValue(dec.remain) + if err == nil { + dec.remain, n = skipSpacesN(r) + dec.inputOffset += int64(len(v) + n) + return + } + if len(r) != 0 { + // Parsing of the next JSON value stopped at a position other + // than the end of the input buffer, which indicaates that a + // syntax error was encountered. + return + } + } + + if err = dec.err; err != nil { + if len(dec.remain) != 0 && err == io.EOF { + err = io.ErrUnexpectedEOF + } + return + } + + if dec.buffer == nil { + dec.buffer = make([]byte, 0, minBufferSize) + } else { + dec.buffer = dec.buffer[:copy(dec.buffer[:cap(dec.buffer)], dec.remain)] + dec.remain = nil + } + + if (cap(dec.buffer) - len(dec.buffer)) < minReadSize { + buf := make([]byte, len(dec.buffer), 2*cap(dec.buffer)) + copy(buf, dec.buffer) + dec.buffer = buf + } + + n, err = io.ReadFull(dec.reader, dec.buffer[len(dec.buffer):cap(dec.buffer)]) + if n > 0 { + dec.buffer = dec.buffer[:len(dec.buffer)+n] + if err != nil { + err = nil + } + } else if err == io.ErrUnexpectedEOF { + err = io.EOF + } + dec.remain, n = skipSpacesN(dec.buffer) + dec.inputOffset += int64(n) + dec.err = err + } +} + +// DisallowUnknownFields is documented at https://golang.org/pkg/encoding/json/#Decoder.DisallowUnknownFields +func (dec *Decoder) DisallowUnknownFields() { dec.flags |= DisallowUnknownFields } + +// UseNumber is documented at https://golang.org/pkg/encoding/json/#Decoder.UseNumber +func (dec *Decoder) UseNumber() { dec.flags |= UseNumber } + +// DontCopyString is an extension to the standard encoding/json package +// which instructs the decoder to not copy strings loaded from the json +// payloads when possible. +func (dec *Decoder) DontCopyString() { dec.flags |= DontCopyString } + +// DontCopyNumber is an extension to the standard encoding/json package +// which instructs the decoder to not copy numbers loaded from the json +// payloads. +func (dec *Decoder) DontCopyNumber() { dec.flags |= DontCopyNumber } + +// DontCopyRawMessage is an extension to the standard encoding/json package +// which instructs the decoder to not allocate RawMessage values in separate +// memory buffers (see the documentation of the DontcopyRawMessage flag for +// more detais). +func (dec *Decoder) DontCopyRawMessage() { dec.flags |= DontCopyRawMessage } + +// DontMatchCaseInsensitiveStructFields is an extension to the standard +// encoding/json package which instructs the decoder to not match object fields +// against struct fields in a case-insensitive way, the field names have to +// match exactly to be decoded into the struct field values. +func (dec *Decoder) DontMatchCaseInsensitiveStructFields() { + dec.flags |= DontMatchCaseInsensitiveStructFields +} + +// ZeroCopy is an extension to the standard encoding/json package which enables +// all the copy optimizations of the decoder. +func (dec *Decoder) ZeroCopy() { dec.flags |= ZeroCopy } + +// InputOffset returns the input stream byte offset of the current decoder position. +// The offset gives the location of the end of the most recently returned token +// and the beginning of the next token. +func (dec *Decoder) InputOffset() int64 { + return dec.inputOffset +} + +// Encoder is documented at https://golang.org/pkg/encoding/json/#Encoder +type Encoder struct { + writer io.Writer + prefix string + indent string + buffer *bytes.Buffer + err error + flags AppendFlags +} + +// NewEncoder is documented at https://golang.org/pkg/encoding/json/#NewEncoder +func NewEncoder(w io.Writer) *Encoder { return &Encoder{writer: w, flags: EscapeHTML | SortMapKeys} } + +// Encode is documented at https://golang.org/pkg/encoding/json/#Encoder.Encode +func (enc *Encoder) Encode(v interface{}) error { + if enc.err != nil { + return enc.err + } + + var err error + var buf = encoderBufferPool.Get().(*encoderBuffer) + + buf.data, err = Append(buf.data[:0], v, enc.flags) + + if err != nil { + encoderBufferPool.Put(buf) + return err + } + + buf.data = append(buf.data, '\n') + b := buf.data + + if enc.prefix != "" || enc.indent != "" { + if enc.buffer == nil { + enc.buffer = new(bytes.Buffer) + enc.buffer.Grow(2 * len(buf.data)) + } else { + enc.buffer.Reset() + } + Indent(enc.buffer, buf.data, enc.prefix, enc.indent) + b = enc.buffer.Bytes() + } + + if _, err := enc.writer.Write(b); err != nil { + enc.err = err + } + + encoderBufferPool.Put(buf) + return err +} + +// SetEscapeHTML is documented at https://golang.org/pkg/encoding/json/#Encoder.SetEscapeHTML +func (enc *Encoder) SetEscapeHTML(on bool) { + if on { + enc.flags |= EscapeHTML + } else { + enc.flags &= ^EscapeHTML + } +} + +// SetIndent is documented at https://golang.org/pkg/encoding/json/#Encoder.SetIndent +func (enc *Encoder) SetIndent(prefix, indent string) { + enc.prefix = prefix + enc.indent = indent +} + +// SetSortMapKeys is an extension to the standard encoding/json package which +// allows the program to toggle sorting of map keys on and off. +func (enc *Encoder) SetSortMapKeys(on bool) { + if on { + enc.flags |= SortMapKeys + } else { + enc.flags &= ^SortMapKeys + } +} + +// SetTrustRawMessage skips value checking when encoding a raw json message. It should only +// be used if the values are known to be valid json, e.g. because they were originally created +// by json.Unmarshal. +func (enc *Encoder) SetTrustRawMessage(on bool) { + if on { + enc.flags |= TrustRawMessage + } else { + enc.flags &= ^TrustRawMessage + } +} + +var encoderBufferPool = sync.Pool{ + New: func() interface{} { return &encoderBuffer{data: make([]byte, 0, 4096)} }, +} + +type encoderBuffer struct{ data []byte } diff --git a/utils/encoding/json/json_test.go b/utils/encoding/json/json_test.go new file mode 100644 index 00000000..e78debed --- /dev/null +++ b/utils/encoding/json/json_test.go @@ -0,0 +1,1626 @@ +package json + +import ( + "bytes" + "compress/gzip" + "encoding" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "io/ioutil" + "math" + "os" + "path/filepath" + "reflect" + "runtime" + "strconv" + "strings" + "testing" + "time" +) + +// The encoding/json package does not export the msg field of json.SyntaxError, +// so we use this replacement type in tests. +type testSyntaxError struct { + msg string + Offset int64 +} + +func (e *testSyntaxError) Error() string { return e.msg } + +var ( + marshal func([]byte, interface{}) ([]byte, error) + unmarshal func([]byte, interface{}) error + escapeHTML bool +) + +func TestMain(m *testing.M) { + var pkg string + flag.StringVar(&pkg, "package", ".", "The name of the package to test (encoding/json, or default to this package)") + flag.BoolVar(&escapeHTML, "escapehtml", false, "Whether to enable HTML escaping or not") + flag.Parse() + + switch pkg { + case "encoding/json": + buf := &buffer{} + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(escapeHTML) + + marshal = func(b []byte, v interface{}) ([]byte, error) { + buf.data = b + err := enc.Encode(v) + return buf.data, err + } + + unmarshal = json.Unmarshal + + default: + flags := AppendFlags(0) + if escapeHTML { + flags |= EscapeHTML + } + + marshal = func(b []byte, v interface{}) ([]byte, error) { + return Append(b, v, flags) + } + + unmarshal = func(b []byte, v interface{}) error { + _, err := Parse(b, v, ZeroCopy) + return err + } + } + + os.Exit(m.Run()) +} + +type point struct { + X int `json:"x"` + Y int `json:"y"` +} + +type tree struct { + Value string + Left *tree + Right *tree +} + +var testValues = [...]interface{}{ + // constants + nil, + false, + true, + + // int + int(0), + int(1), + int(42), + int(-1), + int(-42), + int8(math.MaxInt8), + int8(math.MinInt8), + int16(math.MaxInt16), + int16(math.MinInt16), + int32(math.MaxInt32), + int32(math.MinInt32), + int64(math.MaxInt64), + int64(math.MinInt64), + + // uint + uint(0), + uint(1), + uintptr(0), + uintptr(1), + uint8(math.MaxUint8), + uint16(math.MaxUint16), + uint32(math.MaxUint32), + uint64(math.MaxUint64), + + // float + float32(0), + float32(0.5), + float32(math.SmallestNonzeroFloat32), + float32(math.MaxFloat32), + float64(0), + float64(0.5), + float64(math.SmallestNonzeroFloat64), + float64(math.MaxFloat64), + + // number + Number("0"), + Number("1234567890"), + Number("-0.5"), + Number("-1e+2"), + + // string + "", + "Hello World!", + "Hello\"World!", + "Hello\\World!", + "Hello\nWorld!", + "Hello\rWorld!", + "Hello\tWorld!", + "Hello\bWorld!", + "Hello\fWorld!", + "ไฝ ๅฅฝ", + "<", + ">", + "&", + "\u001944", + "\u00c2e>", + "\u00c2V?", + "\u000e=8", + "\u001944\u00c2e>\u00c2V?\u000e=8", + "ir\u001bQJ\u007f\u0007y\u0015)", + strings.Repeat("A", 32), + strings.Repeat("A", 250), + strings.Repeat("A", 1020), + + // bytes + []byte(""), + []byte("Hello World!"), + bytes.Repeat([]byte("A"), 250), + bytes.Repeat([]byte("A"), 1020), + + // time + time.Unix(0, 0).In(time.UTC), + time.Unix(1, 42).In(time.UTC), + time.Unix(17179869184, 999999999).In(time.UTC), + time.Date(2016, 12, 20, 0, 20, 1, 0, time.UTC), + + // array + [...]int{}, + [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, + + // slice + []int{}, + []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, + makeSlice(250), + makeSlice(1020), + []string{"A", "B", "C"}, + []interface{}{nil, true, false, 0.5, "Hello World!"}, + + // map + makeMapStringBool(0), + makeMapStringBool(15), + makeMapStringBool(1020), + makeMapStringInterface(0), + makeMapStringInterface(15), + makeMapStringInterface(1020), + map[int]bool{1: false, 42: true}, + map[textValue]bool{{1, 2}: true, {3, 4}: false}, + map[string]*point{ + "A": {1, 2}, + "B": {3, 4}, + "C": {5, 6}, + }, + map[string]RawMessage{ + "A": RawMessage(`{}`), + "B": RawMessage(`null`), + "C": RawMessage(`42`), + }, + + // struct + struct{}{}, + struct{ A int }{42}, + struct{ A, B, C int }{1, 2, 3}, + struct { + A int + T time.Time + S string + }{42, time.Date(2016, 12, 20, 0, 20, 1, 0, time.UTC), "Hello World!"}, + // These types are interesting because they fit in a pointer so the compiler + // puts their value directly into the pointer field of the interface{} that + // is passed to Marshal. + struct{ X *int }{}, + struct{ X *int }{new(int)}, + struct{ X **int }{}, + // Struct types with more than one pointer, those exercise the regular + // pointer handling with code that dereferences the fields. + struct{ X, Y *int }{}, + struct{ X, Y *int }{new(int), new(int)}, + struct { + A string `json:"name"` + B string `json:"-"` + C string `json:",omitempty"` + D map[string]interface{} `json:",string"` + e string + }{A: "Luke", D: map[string]interface{}{"answer": float64(42)}}, + struct{ point }{point{1, 2}}, + tree{ + Value: "T", + Left: &tree{Value: "L"}, + Right: &tree{Value: "R", Left: &tree{Value: "R-L"}}, + }, + + // pointer + (*string)(nil), + new(int), + + // Marshaler/Unmarshaler + jsonValue{}, + jsonValue{1, 2}, + + // encoding.TextMarshaler/encoding.TextUnmarshaler + textValue{}, + textValue{1, 2}, + + // RawMessage + RawMessage(`{ + "answer": 42, + "hello": "world" +}`), + + // fixtures + loadTestdata(filepath.Join(runtime.GOROOT(), "src/encoding/json/testdata/code.json.gz")), +} + +var durationTestValues = []interface{}{ + // duration + time.Nanosecond, + time.Microsecond, + time.Millisecond, + time.Second, + time.Minute, + time.Hour, + + // struct with duration + struct{ D1, D2 time.Duration }{time.Millisecond, time.Hour}, +} + +func makeSlice(n int) []int { + s := make([]int, n) + for i := range s { + s[i] = i + } + return s +} + +func makeMapStringBool(n int) map[string]bool { + m := make(map[string]bool, n) + for i := 0; i != n; i++ { + m[strconv.Itoa(i)] = true + } + return m +} + +func makeMapStringInterface(n int) map[string]interface{} { + m := make(map[string]interface{}, n) + for i := 0; i != n; i++ { + m[strconv.Itoa(i)] = nil + } + return m +} + +func testName(v interface{}) string { + return fmt.Sprintf("%T", v) +} + +type codeResponse2 struct { + Tree *codeNode2 `json:"tree"` + Username string `json:"username"` +} + +type codeNode2 struct { + Name string `json:"name"` + Kids []*codeNode `json:"kids"` + CLWeight float64 `json:"cl_weight"` + Touches int `json:"touches"` + MinT int64 `json:"min_t"` + MaxT int64 `json:"max_t"` + MeanT int64 `json:"mean_t"` +} + +func loadTestdata(path string) interface{} { + f, err := os.Open(path) + if err != nil { + return err.Error() + } + defer f.Close() + + r, err := gzip.NewReader(f) + if err != nil { + return err.Error() + } + defer r.Close() + + testdata := new(codeResponse2) + if err := json.NewDecoder(r).Decode(testdata); err != nil { + return err.Error() + } + return testdata +} + +func TestCodec(t *testing.T) { + for _, v1 := range testValues { + t.Run(testName(v1), func(t *testing.T) { + v2 := newValue(v1) + + a, err := json.MarshalIndent(v1, "", "\t") + if err != nil { + t.Error(err) + return + } + a = append(a, '\n') + + buf := &bytes.Buffer{} + enc := NewEncoder(buf) + enc.SetIndent("", "\t") + + if err := enc.Encode(v1); err != nil { + t.Error(err) + return + } + b := buf.Bytes() + + if !Valid(b) { + t.Error("invalid JSON representation") + } + + if !bytes.Equal(a, b) { + t.Error("JSON representations mismatch") + t.Log("expected:", string(a)) + t.Log("found: ", string(b)) + } + + dec := NewDecoder(bytes.NewBuffer(b)) + + if err := dec.Decode(v2.Interface()); err != nil { + t.Errorf("%T: %v", err, err) + return + } + + x1 := v1 + x2 := v2.Elem().Interface() + + if !reflect.DeepEqual(x1, x2) { + t.Error("values mismatch") + t.Logf("expected: %#v", x1) + t.Logf("found: %#v", x2) + } + + if b, err := ioutil.ReadAll(dec.Buffered()); err != nil { + t.Error(err) + } else if len(b) != 0 { + t.Errorf("leftover trailing bytes in the decoder: %q", b) + } + }) + } +} + +// TestCodecDuration isolates testing of time.Duration. The stdlib un/marshals +// this type as integers whereas this library un/marshals formatted string +// values. Therefore, plugging durations into TestCodec would cause fail since +// it checks equality on the marshaled strings from the two libraries. +func TestCodecDuration(t *testing.T) { + for _, v1 := range durationTestValues { + t.Run(testName(v1), func(t *testing.T) { + v2 := newValue(v1) + + // encode using stdlib. (will be an int) + std, err := json.MarshalIndent(v1, "", "\t") + if err != nil { + t.Error(err) + return + } + std = append(std, '\n') + + // decode using our decoder. (reads int to duration) + dec := NewDecoder(bytes.NewBuffer([]byte(std))) + + if err := dec.Decode(v2.Interface()); err != nil { + t.Errorf("%T: %v", err, err) + return + } + + x1 := v1 + x2 := v2.Elem().Interface() + + if !reflect.DeepEqual(x1, x2) { + t.Error("values mismatch") + t.Logf("expected: %#v", x1) + t.Logf("found: %#v", x2) + } + + // encoding using our encoder. (writes duration as string) + buf := &bytes.Buffer{} + enc := NewEncoder(buf) + enc.SetIndent("", "\t") + + if err := enc.Encode(v1); err != nil { + t.Error(err) + return + } + b := buf.Bytes() + + if !Valid(b) { + t.Error("invalid JSON representation") + } + + if reflect.DeepEqual(std, b) { + t.Error("encoded durations should not match stdlib") + t.Logf("got: %s", b) + } + + // decode using our decoder. (reads string to duration) + dec = NewDecoder(bytes.NewBuffer([]byte(std))) + + if err := dec.Decode(v2.Interface()); err != nil { + t.Errorf("%T: %v", err, err) + return + } + + x1 = v1 + x2 = v2.Elem().Interface() + + if !reflect.DeepEqual(x1, x2) { + t.Error("values mismatch") + t.Logf("expected: %#v", x1) + t.Logf("found: %#v", x2) + } + }) + } +} + +func newValue(model interface{}) reflect.Value { + if model == nil { + return reflect.New(reflect.TypeOf(&model).Elem()) + } + return reflect.New(reflect.TypeOf(model)) +} + +func BenchmarkMarshal(b *testing.B) { + j := make([]byte, 0, 128*1024) + + for _, v := range testValues { + b.Run(testName(v), func(b *testing.B) { + if marshal == nil { + return + } + + for i := 0; i != b.N; i++ { + j, _ = marshal(j[:0], v) + } + + b.SetBytes(int64(len(j))) + }) + } +} + +func BenchmarkUnmarshal(b *testing.B) { + for _, v := range testValues { + b.Run(testName(v), func(b *testing.B) { + if unmarshal == nil { + return + } + + x := v + if d, ok := x.(time.Duration); ok { + x = duration(d) + } + + j, _ := json.Marshal(x) + x = newValue(v).Interface() + + for i := 0; i != b.N; i++ { + unmarshal(j, x) + } + + b.SetBytes(int64(len(j))) + }) + } +} + +type buffer struct{ data []byte } + +func (buf *buffer) Write(b []byte) (int, error) { + buf.data = append(buf.data, b...) + return len(b), nil +} + +func (buf *buffer) WriteString(s string) (int, error) { + buf.data = append(buf.data, s...) + return len(s), nil +} + +type jsonValue struct { + x int32 + y int32 +} + +func (v jsonValue) MarshalJSON() ([]byte, error) { + return Marshal([2]int32{v.x, v.y}) +} + +func (v *jsonValue) UnmarshalJSON(b []byte) error { + var a [2]int32 + err := Unmarshal(b, &a) + v.x = a[0] + v.y = a[1] + return err +} + +type textValue struct { + x int32 + y int32 +} + +func (v textValue) MarshalText() ([]byte, error) { + return []byte(fmt.Sprintf("(%d,%d)", v.x, v.y)), nil +} + +func (v *textValue) UnmarshalText(b []byte) error { + _, err := fmt.Sscanf(string(b), "(%d,%d)", &v.x, &v.y) + return err +} + +type duration time.Duration + +func (d duration) MarshalJSON() ([]byte, error) { + return []byte(`"` + time.Duration(d).String() + `"`), nil +} + +func (d *duration) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + x, err := time.ParseDuration(s) + *d = duration(x) + return err +} + +var ( + _ Marshaler = jsonValue{} + _ Marshaler = duration(0) + + _ encoding.TextMarshaler = textValue{} + + _ Unmarshaler = (*jsonValue)(nil) + _ Unmarshaler = (*duration)(nil) + + _ encoding.TextUnmarshaler = (*textValue)(nil) +) + +func TestDecodeStructFieldCaseInsensitive(t *testing.T) { + b := []byte(`{ "type": "changed" }`) + s := struct { + Type string + }{"unchanged"} + + if err := Unmarshal(b, &s); err != nil { + t.Error(err) + } + + if s.Type != "changed" { + t.Error("s.Type: expected to be changed but found", s.Type) + } +} + +func TestDecodeLines(t *testing.T) { + tests := []struct { + desc string + reader io.Reader + expectCount int + }{ + + // simple + + { + desc: "bare object", + reader: strings.NewReader("{\"Good\":true}"), + expectCount: 1, + }, + { + desc: "multiple objects on one line", + reader: strings.NewReader("{\"Good\":true}{\"Good\":true}\n"), + expectCount: 2, + }, + { + desc: "object spanning multiple lines", + reader: strings.NewReader("{\n\"Good\":true\n}\n"), + expectCount: 1, + }, + + // whitespace handling + + { + desc: "trailing newline", + reader: strings.NewReader("{\"Good\":true}\n{\"Good\":true}\n"), + expectCount: 2, + }, + { + desc: "multiple trailing newlines", + reader: strings.NewReader("{\"Good\":true}\n{\"Good\":true}\n\n"), + expectCount: 2, + }, + { + desc: "blank lines", + reader: strings.NewReader("{\"Good\":true}\n\n{\"Good\":true}"), + expectCount: 2, + }, + { + desc: "no trailing newline", + reader: strings.NewReader("{\"Good\":true}\n{\"Good\":true}"), + expectCount: 2, + }, + { + desc: "leading whitespace", + reader: strings.NewReader(" {\"Good\":true}\n\t{\"Good\":true}"), + expectCount: 2, + }, + + // multiple reads + + { + desc: "one object, multiple reads", + reader: io.MultiReader( + strings.NewReader("{"), + strings.NewReader("\"Good\": true"), + strings.NewReader("}\n"), + ), + expectCount: 1, + }, + + // EOF reads + + { + desc: "one object + EOF", + reader: &eofReader{"{\"Good\":true}\n"}, + expectCount: 1, + }, + { + desc: "leading whitespace + EOF", + reader: &eofReader{"\n{\"Good\":true}\n"}, + expectCount: 1, + }, + { + desc: "multiple objects + EOF", + reader: &eofReader{"{\"Good\":true}\n{\"Good\":true}\n"}, + expectCount: 2, + }, + { + desc: "one object + multiple reads + EOF", + reader: io.MultiReader( + strings.NewReader("{"), + strings.NewReader(" \"Good\": true"), + &eofReader{"}\n"}, + ), + expectCount: 1, + }, + { + desc: "multiple objects + multiple reads + EOF", + reader: io.MultiReader( + strings.NewReader("{"), + strings.NewReader(" \"Good\": true}{\"Good\": true}"), + &eofReader{"\n"}, + ), + expectCount: 2, + }, + + { + // the 2nd object should be discarded, as 42 cannot be cast to bool + desc: "unmarshal error while decoding", + reader: strings.NewReader("{\"Good\":true}\n{\"Good\":42}\n{\"Good\":true}\n"), + expectCount: 2, + }, + { + // the 2nd object should be discarded, as 42 cannot be cast to bool + desc: "unmarshal error while decoding last object", + reader: strings.NewReader("{\"Good\":true}\n{\"Good\":42}\n"), + expectCount: 1, + }, + } + + type obj struct { + Good bool + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + d := NewDecoder(test.reader) + var count int + var err error + for { + var o obj + err = d.Decode(&o) + if err != nil { + if err == io.EOF { + break + } + + switch err.(type) { + case *SyntaxError, *UnmarshalTypeError, *UnmarshalFieldError: + t.Log("unmarshal error", err) + continue + } + + t.Error("decode error", err) + break + } + if !o.Good { + t.Errorf("object was not unmarshaled correctly: %#v", o) + } + count++ + } + + if err != nil && err != io.EOF { + t.Error(err) + } + + if count != test.expectCount { + t.Errorf("expected %d objects, got %d", test.expectCount, count) + } + }) + } +} + +// eofReader is a simple io.Reader that reads its full contents _and_ returns +// and EOF in the first call. Subsequent Read calls only return EOF. +type eofReader struct { + s string +} + +func (r *eofReader) Read(p []byte) (n int, err error) { + n = copy(p, r.s) + r.s = r.s[n:] + if r.s == "" { + err = io.EOF + } + return +} + +func TestDontMatchCaseIncensitiveStructFields(t *testing.T) { + b := []byte(`{ "type": "changed" }`) + s := struct { + Type string + }{"unchanged"} + + if _, err := Parse(b, &s, DontMatchCaseInsensitiveStructFields); err != nil { + t.Error(err) + } + + if s.Type != "unchanged" { + t.Error("s.Type: expected to be unchanged but found", s.Type) + } +} + +func TestMarshalFuzzBugs(t *testing.T) { + tests := []struct { + value interface{} + output string + }{ + { // html sequences are escaped even in RawMessage + value: struct { + P RawMessage + }{P: RawMessage(`"<"`)}, + output: "{\"P\":\"\\u003c\"}", + }, + { // raw message output is compacted + value: struct { + P RawMessage + }{P: RawMessage(`{"" :{}}`)}, + output: "{\"P\":{\"\":{}}}", + }, + } + + for _, test := range tests { + t.Run("", func(t *testing.T) { + b, err := Marshal(test.value) + if err != nil { + t.Fatal(err) + } + + if string(b) != test.output { + t.Error("values mismatch") + t.Logf("expected: %#v", test.output) + t.Logf("found: %#v", string(b)) + } + }) + } +} + +func TestUnmarshalFuzzBugs(t *testing.T) { + tests := []struct { + input string + value interface{} + }{ + { // non-UTF8 sequences must be converted to the utf8.RuneError character. + input: "[\"00000\xef\"]", + value: []interface{}{"00000๏ฟฝ"}, + }, + { // UTF16 surrogate followed by null character + input: "[\"\\ud800\\u0000\"]", + value: []interface{}{"๏ฟฝ\x00"}, + }, + { // UTF16 surrogate followed by ascii character + input: "[\"\\uDF00\\u000e\"]", + value: []interface{}{"๏ฟฝ\x0e"}, + }, + { // UTF16 surrogate followed by unicode character + input: "[[\"\\uDF00\\u0800\"]]", + value: []interface{}{[]interface{}{"๏ฟฝเ €"}}, + }, + { // invalid UTF16 surrogate sequenced followed by a valid UTF16 surrogate sequence + input: "[\"\\udf00\\udb00\\udf00\"]", + value: []interface{}{"๏ฟฝ\U000d0300"}, + }, + { // decode single-element slice into []byte field + input: "{\"f\":[0],\"0\":[0]}", + value: struct{ F []byte }{F: []byte{0}}, + }, + { // decode multi-element slice into []byte field + input: "{\"F\":[3,1,1,1,9,9]}", + value: struct{ F []byte }{F: []byte{3, 1, 1, 1, 9, 9}}, + }, + { // decode string with escape sequence into []byte field + input: "{\"F\":\"0p00\\r\"}", + value: struct{ F []byte }{F: []byte("า4")}, + }, + { // decode unicode code points which fold into ascii characters + input: "{\"ลฟ\":\"8\"}", + value: struct { + S int `json:",string"` + }{S: 8}, + }, + { // decode unicode code points which don't fold into ascii characters + input: "{\"ฤฐ\":\"\"}", + value: struct{ I map[string]string }{I: nil}, + }, + { // override pointer-to-pointer field clears the inner pointer only + input: "{\"o\":0,\"o\":null}", + value: struct{ O **int }{O: new(*int)}, + }, + { // subsequent occurrences of a map field retain keys previously loaded + input: "{\"i\":{\"\":null},\"i\":{}}", + value: struct{ I map[string]string }{I: map[string]string{"": ""}}, + }, + { // an empty string is an invalid JSON input + input: "", + }, + { // ASCII character below 0x20 are invalid JSON input + input: "[\"\b\"]", + }, + { // random byte before any value + input: "\xad", + }, + { // cloud be the beginning of a false value but not + input: "f", + value: false, + }, + { // random ASCII character + input: "}", + value: []interface{}{}, + }, + { // random byte after valid JSON, decoded to a nil type + input: "0\x93", + }, + { // random byte after valid JSON, decoded to a int type + input: "0\x93", + value: 0, + }, + { // random byte after valid JSON, decoded to a slice type + input: "0\x93", + value: []interface{}{}, + }, + { // decode integer into slice + input: "0", + value: []interface{}{}, + }, + { // decode integer with trailing space into slice + input: "0\t", + value: []interface{}{}, + }, + { // decode integer with leading random bytes into slice + input: "\b0", + value: []interface{}{}, + }, + { // decode string into slice followed by number + input: "\"\"0", + value: []interface{}{}, + }, + { // decode what looks like an object followed by a number into a string + input: "{0", + value: "", + }, + { // decode what looks like an object followed by a number into a map + input: "{0", + value: map[string]string{}, + }, + { // decode string into string with trailing random byte + input: "\"\"\f", + value: "", + }, + { // decode weird number value into nil + input: "-00", + }, + { // decode an invalid escaped sequence + input: "\"\\0\"", + value: "", + }, + { // decode what looks like an array followed by a number into a slice + input: "[9E600", + value: []interface{}{}, + }, + { // decode a number which is too large to fit in a float64 + input: "[1e900]", + value: []interface{}{}, + }, + { // many nested arrays openings + input: "[[[[[[", + value: []interface{}{}, + }, + { // decode a map with value type mismatch and missing closing character + input: "{\"\":0", + value: map[string]string{}, + }, + { // decode a struct with value type mismatch and missing closing character + input: "{\"E\":\"\"", + value: struct{ E uint8 }{}, + }, + { // decode a map with value type mismatch + input: "{\"\":0}", + value: map[string]string{}, + }, + { // decode number with exponent into integer field + input: "{\"e\":0e0}", + value: struct{ E uint8 }{}, + }, + { // decode invalid integer representation into integer field + input: "{\"e\":00}", + value: struct{ E uint8 }{}, + }, + { // decode unterminated array into byte slice + input: "{\"F\":[", + value: struct{ F []byte }{}, + }, + { // attempt to decode string into in + input: "{\"S\":\"\"}", + value: struct { + S int `json:",string"` + }{}, + }, + { // decode object with null key into map + input: "{null:0}", + value: map[string]interface{}{}, + }, + { // decode unquoted integer into struct field with string tag + input: "{\"S\":0}", + value: struct { + S int `json:",string"` + }{}, + }, + { // invalid base64 content when decoding string into byte slice + input: "{\"F\":\"0\"}", + value: struct{ F []byte }{}, + }, + { // decode an object with a "null" string as key + input: "{\"null\":null}", + value: struct { + S int `json:",string"` + }{}, + }, + { // decode an invalid floating point number representation into an integer field with string tag + input: "{\"s\":8e800}", + value: struct { + S int `json:",string"` + }{}, + }, + { // decode a string with leading zeroes into an integer field with string tag + input: "{\"S\":\"00\"}", + value: struct { + S int `json:",string"` + }{}, + }, + { // decode a string with invalid leading sign and zeroes into an integer field with string tag + input: "{\"S\":\"+00\"}", + value: struct { + S int `json:",string"` + }{}, + }, + { // decode a string with valid leading sign and zeroes into an integer field with string tag + input: "{\"S\":\"-00\"}", + value: struct { + S int `json:",string"` + }{}, + }, + { // decode non-ascii string into integer field with string tag + input: "{\"ลฟ\":\"\xbf\"}", + value: struct { + S int `json:",string"` + }{}, + }, + { // decode a valid floating point number representation into an integer field with string tag + input: "{\"S\":0.0}", + value: struct { + S int `json:",string"` + }{}, + }, + { // decode string with invalid leading sign to integer field with string tag + input: "{\"S\":\"+0\"}", + value: struct { + S int `json:",string"` + }{}, + }, + { // decode string with valid leading sign to integer field with string tag + input: "{\"S\":\"-0\"}", + value: struct { + S int `json:",string"` + }{}, + }, + { // decode string with object representation to integer field with string tag + input: "{\"s\":{}}", + value: struct { + S int `json:",string"` + }{}, + }, + { // decoding integer with leading zeroes + input: "{\"o\":00}", + value: struct{ O **int }{}, + }, + { // codeding string with invalid float representation into integer field with string tag + input: "{\"s\":\"0.\"}", + value: struct { + S int `json:",string"` + }{}, + }, + { // malformed negative integer in object value + input: "{\"N\":-00}", + value: struct{ N *int }{}, + }, + { // integer overflow + input: "{\"a\":9223372036854775808}", + value: struct { + A int `json:",omitempty"` + }{}, + }, + { // decode string with number followed by random byte into integer field with string tag + input: "{\"s\":\"0]\"}", + value: struct { + S int `json:",string"` + }{}, + }, + { // decode object into integer field + input: "{\"n\":{}}", + value: struct{ N *int }{}, + }, + { // decode negative integer into unsigned type + input: "{\"E\":-0}", + value: struct{ E uint8 }{}, + }, + { // decode string with number followed by random byte into integer field with string tag + input: "{\"s\":\"03๏ฟฝ\"}", + value: struct { + S int `json:",string"` + }{}, + }, + { // decode string with leading zeroes into integer field with string tag + input: "{\"s\":\"03\"}", + value: struct { + S int `json:",string"` + }{S: 3}, + }, + { // decode string containing what looks like an object into integer field with string tag + input: "{\"S\":\"{}\"}", + value: struct { + S int `json:",string"` + }{}, + }, + { // decode an empty string followed by the same field with a null value into a byte slice + input: "{\"F\":\"\",\"F\":null}", + value: struct{ F []byte }{}, + }, + { // decode string containing a float into an integer field with string tag + input: "{\"S\":\"0e0\"}", + value: struct { + S int `json:",string"` + }{}, + }, + { // decode string with negative sign into a an integer field with string tag + input: "{\"s\":\"-\"}", + value: struct { + S int `json:",string"` + }{}, + }, + { // decode string with positive sign into a an integer field with string tag + input: "{\"s\":\"+\"}", + value: struct { + S int `json:",string"` + }{}, + }, + { // decode an integer into a json unmarshaler + input: "{\"q\":0}", + value: struct { + Q testMarshaller + }{}, + }, + // This test fails because it appears that the encoding/json package + // will decode "q" before "s", so it returns an error about "q" being of + // the wrong type while this package will prase object keys in the order + // that they appear in the JSON input, so it detects the error from "s" + // first. + // + //{ + // input: "{\"s\":0,\"q\":0}", + // value: struct { + // Q testMarshaller + // S int `json:",string"` + // }{}, + //}, + } + + for _, test := range tests { + t.Run("", func(t *testing.T) { + var ptr1 interface{} + var ptr2 interface{} + + if test.value != nil { + ptr1 = reflect.New(reflect.TypeOf(test.value)).Interface() + ptr2 = reflect.New(reflect.TypeOf(test.value)).Interface() + } + + err1 := json.Unmarshal([]byte(test.input), ptr1) + err2 := Unmarshal([]byte(test.input), ptr2) + + if reflect.TypeOf(err1) != reflect.TypeOf(err2) { + t.Error("errors mismatch") + t.Logf("expected: %T: %v", err1, err1) + t.Logf("found: %T: %v", err2, err2) + } else if err1 == nil && test.value != nil { + if value := reflect.ValueOf(ptr2).Elem().Interface(); !reflect.DeepEqual(test.value, value) { + t.Error("values mismatch") + t.Logf("expected: %#v", test.value) + t.Logf("found: %#v", value) + } + } + }) + } +} + +func BenchmarkEasyjsonUnmarshalSmallStruct(b *testing.B) { + type Hashtag struct { + Indices []int `json:"indices"` + Text string `json:"text"` + } + + //easyjson:json + type Entities struct { + Hashtags []Hashtag `json:"hashtags"` + Urls []*string `json:"urls"` + UserMentions []*string `json:"user_mentions"` + } + + var json = []byte(`{"hashtags":[{"indices":[5, 10],"text":"some-text"}],"urls":[],"user_mentions":[]}`) + + for i := 0; i < b.N; i++ { + var value Entities + if err := Unmarshal(json, &value); err != nil { + b.Fatal(err) + } + } +} + +type testMarshaller struct { + v string +} + +func (m *testMarshaller) MarshalJSON() ([]byte, error) { + return Marshal(m.v) +} + +func (m *testMarshaller) UnmarshalJSON(data []byte) error { + return Unmarshal(data, &m.v) +} + +func TestGithubIssue11(t *testing.T) { + // https://github.com/segmentio/encoding/issues/11 + v := struct{ F float64 }{ + F: math.NaN(), + } + + _, err := Marshal(v) + if err == nil { + t.Error("no error returned when marshalling NaN value") + } else if s := err.Error(); !strings.Contains(s, "NaN") { + t.Error("error returned when marshalling NaN value does not mention 'NaN':", s) + } else { + t.Log(s) + } +} + +type Issue13 struct { + Stringer fmt.Stringer + Field int `json:"MyInt"` +} + +type S string + +func (s S) String() string { return string(s) } + +func TestGithubIssue13(t *testing.T) { + // https://github.com/segmentio/encoding/issues/13 + v := Issue13{} + + b, err := Marshal(v) + if err != nil { + t.Error("unexpected errror:", err) + } else { + t.Log(string(b)) + } + + v = Issue13{Stringer: S("")} + if err := Unmarshal([]byte(`{"Stringer":null}`), &v); err != nil { + t.Error("unexpected error:", err) + } + if v.Stringer != nil { + t.Error("Stringer field was not overwritten") + } + + v = Issue13{} + if err := Unmarshal([]byte(`{"Stringer":"whatever"}`), &v); err == nil { + t.Error("expected error but decoding string value into nil fmt.Stringer but got ") + } + + v = Issue13{Stringer: S("")} + if err := Unmarshal([]byte(`{"Stringer":"whatever"}`), &v); err == nil { + t.Error("expected error but decoding string value into non-pointer fmt.Stringer but got ") + } + + s := S("") + v = Issue13{Stringer: &s} + if err := Unmarshal([]byte(`{"Stringer":"whatever"}`), &v); err != nil { + t.Error("unexpected error decoding string value into pointer fmt.Stringer:", err) + } +} + +func TestGithubIssue15(t *testing.T) { + // https://github.com/segmentio/encoding/issues/15 + tests := []struct { + m interface{} + s string + }{ + { + m: map[uint]bool{1: true, 123: true, 333: true, 42: true}, + s: `{"1":true,"123":true,"333":true,"42":true}`, + }, + { + m: map[int]bool{-1: true, -123: true, 333: true, 42: true}, + s: `{"-1":true,"-123":true,"333":true,"42":true}`, + }, + } + + for _, test := range tests { + b, _ := Marshal(test.m) + + if string(b) != test.s { + t.Error("map with integer keys must be ordered by their string representation, got", string(b)) + } + + } +} + +type sliceA []byte + +func (sliceA) MarshalJSON() ([]byte, error) { + return []byte(`"A"`), nil +} + +type sliceB []byte + +func (sliceB) MarshalText() ([]byte, error) { + return []byte("B"), nil +} + +type mapA map[string]string + +func (mapA) MarshalJSON() ([]byte, error) { + return []byte(`"A"`), nil +} + +type mapB map[string]string + +func (mapB) MarshalText() ([]byte, error) { + return []byte("B"), nil +} + +type intPtrA int + +func (*intPtrA) MarshalJSON() ([]byte, error) { + return []byte(`"A"`), nil +} + +type intPtrB int + +func (*intPtrB) MarshalText() ([]byte, error) { + return []byte("B"), nil +} + +type structA struct{ I intPtrA } +type structB struct{ I intPtrB } +type structC struct{ M Marshaler } +type structD struct{ M encoding.TextMarshaler } + +func TestGithubIssue16(t *testing.T) { + // https://github.com/segmentio/encoding/issues/16 + tests := []struct { + value interface{} + output string + }{ + {value: sliceA(nil), output: `"A"`}, + {value: sliceB(nil), output: `"B"`}, + {value: mapA(nil), output: `"A"`}, + {value: mapB(nil), output: `"B"`}, + {value: intPtrA(1), output: `1`}, + {value: intPtrB(2), output: `2`}, + {value: new(intPtrA), output: `"A"`}, + {value: new(intPtrB), output: `"B"`}, + {value: (*intPtrA)(nil), output: `null`}, + {value: (*intPtrB)(nil), output: `null`}, + {value: structA{I: 1}, output: `{"I":1}`}, + {value: structB{I: 2}, output: `{"I":2}`}, + {value: structC{}, output: `{"M":null}`}, + {value: structD{}, output: `{"M":null}`}, + {value: &structA{I: 1}, output: `{"I":"A"}`}, + {value: &structB{I: 2}, output: `{"I":"B"}`}, + {value: &structC{}, output: `{"M":null}`}, + {value: &structD{}, output: `{"M":null}`}, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("%T", test.value), func(t *testing.T) { + if b, _ := Marshal(test.value); string(b) != test.output { + t.Errorf(`%s != %s`, string(b), test.output) + } + }) + } +} + +func TestDecoderInputOffset(t *testing.T) { + checkOffset := func(o, expected int64) { + if o != expected { + t.Error("unexpected input offset", o, expected) + } + } + + b := []byte(`{"userId": "blah"}{"userId": "blah"} + {"userId": "blah"}{"num": 0}`) + d := NewDecoder(bytes.NewReader(b)) + + var expected int64 + checkOffset(d.InputOffset(), expected) + + var a struct { + UserId string `json:"userId"` + } + + if err := d.Decode(&a); err != nil { + t.Error("unexpected decode error", err) + } + expected = int64(18) + checkOffset(d.InputOffset(), expected) + + if err := d.Decode(&a); err != nil { + t.Error("unexpected decode error", err) + } + expected = int64(38) + checkOffset(d.InputOffset(), expected) + + if err := d.Decode(&a); err != nil { + t.Error("unexpected decode error", err) + } + expected = int64(56) + checkOffset(d.InputOffset(), expected) + + var z struct { + Num int64 `json:"num"` + } + if err := d.Decode(&z); err != nil { + t.Error("unexpected decode error", err) + } + expected = int64(66) + checkOffset(d.InputOffset(), expected) +} + +func TestGithubIssue18(t *testing.T) { + // https://github.com/segmentio/encoding/issues/18 + b := []byte(`{ + "userId": "blah", + }`) + + d := NewDecoder(bytes.NewReader(b)) + + var a struct { + UserId string `json:"userId"` + } + switch err := d.Decode(&a).(type) { + case *SyntaxError: + default: + t.Error("expected syntax error but found:", err) + } + + for i := 1; i <= 18; i++ { // up to the invalid ',' character + d := NewDecoder(bytes.NewReader(b[:i])) // cut somewhere in the middle + switch err := d.Decode(&a); err { + case io.ErrUnexpectedEOF: + default: + t.Error("expected 'unexpected EOF' error but found:", err) + } + } +} + +func TestGithubIssue23(t *testing.T) { + t.Run("marshal-1", func(t *testing.T) { + type d struct{ S map[string]string } + + b, _ := Marshal(map[string]d{"1": {S: map[string]string{"2": "3"}}}) + if string(b) != `{"1":{"S":{"2":"3"}}}` { + t.Error(string(b)) + } + }) + + t.Run("marshal-2", func(t *testing.T) { + type testInner struct { + InnerMap map[string]string `json:"inner_map"` + } + + type testOuter struct { + OuterMap map[string]testInner `json:"outer_map"` + } + + b, _ := Marshal(testOuter{ + OuterMap: map[string]testInner{ + "outer": { + InnerMap: map[string]string{"inner": "value"}, + }, + }, + }) + + if string(b) != `{"outer_map":{"outer":{"inner_map":{"inner":"value"}}}}` { + t.Error(string(b)) + } + }) + + t.Run("marshal-3", func(t *testing.T) { + type A struct{ A map[string]string } + type B struct{ B map[string]A } + type C struct{ C map[string]B } + + b, _ := Marshal(C{ + C: map[string]B{ + "1": B{ + B: map[string]A{ + "2": A{ + A: map[string]string{"3": "!"}, + }, + }, + }, + }, + }) + + if string(b) != `{"C":{"1":{"B":{"2":{"A":{"3":"!"}}}}}}` { + t.Error(string(b)) + } + }) + + t.Run("unmarshal-1", func(t *testing.T) { + var d struct{ S map[string]string } + + if err := Unmarshal([]byte(`{"1":{"S":{"2":"3"}}}`), &d); err != nil { + t.Error(err) + } + }) +} + +func TestGithubIssue26(t *testing.T) { + type interfaceType interface{} + + var value interfaceType + var data = []byte(`{}`) + + if err := Unmarshal(data, &value); err != nil { + t.Error(err) + } +} + +func TestGithubIssue28(t *testing.T) { + type A struct { + Err error `json:"err"` + } + + if b, err := Marshal(&A{Err: errors.New("ABC")}); err != nil { + t.Error(err) + } else if string(b) != `{"err":{}}` { + t.Error(string(b)) + } + +} + +func TestGithubIssue41(t *testing.T) { + expectedString := `{"Zero":0,"Three":3}` + type M struct { + One int + Two int + } + type N struct { + Zero int + *M + Three int + } + + if b, err := Marshal(N{Three: 3}); err != nil { + t.Error(err) + } else if string(b) != expectedString { + t.Error( + "got: ", string(b), + "expected: ", expectedString, + ) + } + +} + +func TestGithubIssue44(t *testing.T) { + var out rawJsonString + if err := Unmarshal([]byte("null"), &out); err != nil { + t.Fatal(err) + } + if out != "null" { + t.Errorf("wanted \"null\" but got %q", out) + } +} + +type rawJsonString string + +func (r *rawJsonString) UnmarshalJSON(b []byte) error { + if len(b) == 0 { + *r = "null" + } else { + *r = rawJsonString(b) + } + return nil +} + +func TestSetTrustRawMessage(t *testing.T) { + buf := &bytes.Buffer{} + enc := NewEncoder(buf) + enc.SetTrustRawMessage(true) + + // "Good" values are encoded in the regular way + m := map[string]json.RawMessage{ + "k": json.RawMessage(`"value"`), + } + if err := enc.Encode(m); err != nil { + t.Error(err) + } + + b := buf.Bytes() + exp := []byte(`{"k":"value"}`) + exp = append(exp, '\n') + if bytes.Compare(exp, b) != 0 { + t.Error( + "unexpected encoding:", + "expected", exp, + "got", b, + ) + } + + // "Bad" values are encoded without checking and throwing an error + buf.Reset() + m = map[string]json.RawMessage{ + "k": json.RawMessage(`bad"value`), + } + if err := enc.Encode(m); err != nil { + t.Error(err) + } + + b = buf.Bytes() + exp = []byte(`{"k":bad"value}`) + exp = append(exp, '\n') + if bytes.Compare(exp, b) != 0 { + t.Error( + "unexpected encoding:", + "expected", exp, + "got", b, + ) + } +} diff --git a/utils/encoding/json/parse.go b/utils/encoding/json/parse.go new file mode 100644 index 00000000..3ca42184 --- /dev/null +++ b/utils/encoding/json/parse.go @@ -0,0 +1,737 @@ +package json + +import ( + "bytes" + "math" + "reflect" + "unicode" + "unicode/utf16" + "unicode/utf8" + + "github.com/gofiber/fiber/v2/utils/encoding/ascii" +) + +// All spaces characters defined in the json specification. +const ( + sp = ' ' + ht = '\t' + nl = '\n' + cr = '\r' +) + +const ( + escape = '\\' + quote = '"' +) + +func skipSpaces(b []byte) []byte { + b, _ = skipSpacesN(b) + return b +} + +func skipSpacesN(b []byte) ([]byte, int) { + for i := range b { + switch b[i] { + case sp, ht, nl, cr: + default: + return b[i:], i + } + } + return nil, 0 +} + +// parseInt parses a decimanl representation of an int64 from b. +// +// The function is equivalent to calling strconv.ParseInt(string(b), 10, 64) but +// it prevents Go from making a memory allocation for converting a byte slice to +// a string (escape analysis fails due to the error returned by strconv.ParseInt). +// +// Because it only works with base 10 the function is also significantly faster +// than strconv.ParseInt. +func parseInt(b []byte, t reflect.Type) (int64, []byte, error) { + var value int64 + var count int + + if len(b) == 0 { + return 0, b, syntaxError(b, "cannot decode integer from an empty input") + } + + if b[0] == '-' { + const max = math.MinInt64 + const lim = max / 10 + + if len(b) == 1 { + return 0, b, syntaxError(b, "cannot decode integer from '-'") + } + + if len(b) > 2 && b[1] == '0' && '0' <= b[2] && b[2] <= '9' { + return 0, b, syntaxError(b, "invalid leading character '0' in integer") + } + + for _, d := range b[1:] { + if !(d >= '0' && d <= '9') { + if count == 0 { + b, err := inputError(b, t) + return 0, b, err + } + break + } + + if value < lim { + return 0, b, unmarshalOverflow(b, t) + } + + value *= 10 + x := int64(d - '0') + + if value < (max + x) { + return 0, b, unmarshalOverflow(b, t) + } + + value -= x + count++ + } + + count++ + } else { + const max = math.MaxInt64 + const lim = max / 10 + + if len(b) > 1 && b[0] == '0' && '0' <= b[1] && b[1] <= '9' { + return 0, b, syntaxError(b, "invalid leading character '0' in integer") + } + + for _, d := range b { + if !(d >= '0' && d <= '9') { + if count == 0 { + b, err := inputError(b, t) + return 0, b, err + } + break + } + x := int64(d - '0') + + if value > lim { + return 0, b, unmarshalOverflow(b, t) + } + + if value *= 10; value > (max - x) { + return 0, b, unmarshalOverflow(b, t) + } + + value += x + count++ + } + } + + if count < len(b) { + switch b[count] { + case '.', 'e', 'E': // was this actually a float? + v, r, err := parseNumber(b) + if err != nil { + v, r = b[:count+1], b[count+1:] + } + return 0, r, unmarshalTypeError(v, t) + } + } + + return value, b[count:], nil +} + +// parseUint is like parseInt but for unsigned integers. +func parseUint(b []byte, t reflect.Type) (uint64, []byte, error) { + const max = math.MaxUint64 + const lim = max / 10 + + var value uint64 + var count int + + if len(b) == 0 { + return 0, b, syntaxError(b, "cannot decode integer value from an empty input") + } + + if len(b) > 1 && b[0] == '0' && '0' <= b[1] && b[1] <= '9' { + return 0, b, syntaxError(b, "invalid leading character '0' in integer") + } + + for _, d := range b { + if !(d >= '0' && d <= '9') { + if count == 0 { + b, err := inputError(b, t) + return 0, b, err + } + break + } + x := uint64(d - '0') + + if value > lim { + return 0, b, unmarshalOverflow(b, t) + } + + if value *= 10; value > (max - x) { + return 0, b, unmarshalOverflow(b, t) + } + + value += x + count++ + } + + if count < len(b) { + switch b[count] { + case '.', 'e', 'E': // was this actually a float? + v, r, err := parseNumber(b) + if err != nil { + v, r = b[:count+1], b[count+1:] + } + return 0, r, unmarshalTypeError(v, t) + } + } + + return value, b[count:], nil +} + +// parseUintHex parses a hexadecimanl representation of a uint64 from b. +// +// The function is equivalent to calling strconv.ParseUint(string(b), 16, 64) but +// it prevents Go from making a memory allocation for converting a byte slice to +// a string (escape analysis fails due to the error returned by strconv.ParseUint). +// +// Because it only works with base 16 the function is also significantly faster +// than strconv.ParseUint. +func parseUintHex(b []byte) (uint64, []byte, error) { + const max = math.MaxUint64 + const lim = max / 0x10 + + var value uint64 + var count int + + if len(b) == 0 { + return 0, b, syntaxError(b, "cannot decode hexadecimal value from an empty input") + } + +parseLoop: + for i, d := range b { + var x uint64 + + switch { + case d >= '0' && d <= '9': + x = uint64(d - '0') + + case d >= 'A' && d <= 'F': + x = uint64(d-'A') + 0xA + + case d >= 'a' && d <= 'f': + x = uint64(d-'a') + 0xA + + default: + if i == 0 { + return 0, b, syntaxError(b, "expected hexadecimal digit but found '%c'", d) + } + break parseLoop + } + + if value > lim { + return 0, b, syntaxError(b, "hexadecimal value out of range") + } + + if value *= 0x10; value > (max - x) { + return 0, b, syntaxError(b, "hexadecimal value out of range") + } + + value += x + count++ + } + + return value, b[count:], nil +} + +func parseNull(b []byte) ([]byte, []byte, error) { + if hasNullPrefix(b) { + return b[:4], b[4:], nil + } + if len(b) < 4 { + return nil, b[len(b):], unexpectedEOF(b) + } + return nil, b, syntaxError(b, "expected 'null' but found invalid token") +} + +func parseTrue(b []byte) ([]byte, []byte, error) { + if hasTruePrefix(b) { + return b[:4], b[4:], nil + } + if len(b) < 4 { + return nil, b[len(b):], unexpectedEOF(b) + } + return nil, b, syntaxError(b, "expected 'true' but found invalid token") +} + +func parseFalse(b []byte) ([]byte, []byte, error) { + if hasFalsePrefix(b) { + return b[:5], b[5:], nil + } + if len(b) < 5 { + return nil, b[len(b):], unexpectedEOF(b) + } + return nil, b, syntaxError(b, "expected 'false' but found invalid token") +} + +func parseNumber(b []byte) (v, r []byte, err error) { + if len(b) == 0 { + r, err = b, unexpectedEOF(b) + return + } + + i := 0 + // sign + if b[i] == '-' { + i++ + } + + if i == len(b) { + r, err = b[i:], syntaxError(b, "missing number value after sign") + return + } + + if b[i] < '0' || b[i] > '9' { + r, err = b[i:], syntaxError(b, "expected digit but got '%c'", b[i]) + return + } + + // integer part + if b[i] == '0' { + i++ + if i == len(b) || (b[i] != '.' && b[i] != 'e' && b[i] != 'E') { + v, r = b[:i], b[i:] + return + } + if '0' <= b[i] && b[i] <= '9' { + r, err = b[i:], syntaxError(b, "cannot decode number with leading '0' character") + return + } + } + + for i < len(b) && '0' <= b[i] && b[i] <= '9' { + i++ + } + + // decimal part + if i < len(b) && b[i] == '.' { + i++ + decimalStart := i + + for i < len(b) { + if c := b[i]; !('0' <= c && c <= '9') { + if i == decimalStart { + r, err = b[i:], syntaxError(b, "expected digit but found '%c'", c) + return + } + break + } + i++ + } + + if i == decimalStart { + r, err = b[i:], syntaxError(b, "expected decimal part after '.'") + return + } + } + + // exponent part + if i < len(b) && (b[i] == 'e' || b[i] == 'E') { + i++ + + if i < len(b) { + if c := b[i]; c == '+' || c == '-' { + i++ + } + } + + if i == len(b) { + r, err = b[i:], syntaxError(b, "missing exponent in number") + return + } + + exponentStart := i + + for i < len(b) { + if c := b[i]; !('0' <= c && c <= '9') { + if i == exponentStart { + err = syntaxError(b, "expected digit but found '%c'", c) + return + } + break + } + i++ + } + } + + v, r = b[:i], b[i:] + return +} + +func parseUnicode(b []byte) (rune, int, error) { + if len(b) < 4 { + return 0, 0, syntaxError(b, "unicode code point must have at least 4 characters") + } + + u, r, err := parseUintHex(b[:4]) + if err != nil { + return 0, 0, syntaxError(b, "parsing unicode code point: %s", err) + } + + if len(r) != 0 { + return 0, 0, syntaxError(b, "invalid unicode code point") + } + + return rune(u), 4, nil +} + +func parseStringFast(b []byte) ([]byte, []byte, bool, error) { + if len(b) < 2 { + return nil, b[len(b):], false, unexpectedEOF(b) + } + if b[0] != '"' { + return nil, b, false, syntaxError(b, "expected '\"' at the beginning of a string value") + } + + n := bytes.IndexByte(b[1:], '"') + 2 + if n <= 1 { + return nil, b[len(b):], false, syntaxError(b, "missing '\"' at the end of a string value") + } + if bytes.IndexByte(b[1:n], '\\') < 0 && ascii.ValidPrint(b[1:n]) { + return b[:n], b[n:], false, nil + } + + for i := 1; i < len(b); i++ { + switch b[i] { + case '\\': + if i++; i < len(b) { + switch b[i] { + case '"', '\\', '/', 'n', 'r', 't', 'f', 'b': + case 'u': + _, n, err := parseUnicode(b[i+1:]) + if err != nil { + return nil, b, false, err + } + i += n + default: + return nil, b, false, syntaxError(b, "invalid character '%c' in string escape code", b[i]) + } + } + + case '"': + return b[:i+1], b[i+1:], true, nil + + default: + if b[i] < 0x20 { + return nil, b, false, syntaxError(b, "invalid character '%c' in string escape code", b[i]) + } + } + } + + return nil, b[len(b):], false, syntaxError(b, "missing '\"' at the end of a string value") +} + +func parseString(b []byte) ([]byte, []byte, error) { + s, b, _, err := parseStringFast(b) + return s, b, err +} + +func parseStringUnquote(b []byte, r []byte) ([]byte, []byte, bool, error) { + s, b, escaped, err := parseStringFast(b) + if err != nil { + return s, b, false, err + } + + s = s[1 : len(s)-1] // trim the quotes + + if !escaped { + return s, b, false, nil + } + + if r == nil { + r = make([]byte, 0, len(s)) + } + + for len(s) != 0 { + i := bytes.IndexByte(s, '\\') + + if i < 0 { + r = appendCoerceInvalidUTF8(r, s) + break + } + + r = appendCoerceInvalidUTF8(r, s[:i]) + s = s[i+1:] + + c := s[0] + switch c { + case '"', '\\', '/': + // simple escaped character + case 'n': + c = '\n' + + case 'r': + c = '\r' + + case 't': + c = '\t' + + case 'b': + c = '\b' + + case 'f': + c = '\f' + + case 'u': + s = s[1:] + + r1, n1, err := parseUnicode(s) + if err != nil { + return r, b, true, err + } + s = s[n1:] + + if utf16.IsSurrogate(r1) { + if !hasPrefix(s, `\u`) { + r1 = unicode.ReplacementChar + } else { + r2, n2, err := parseUnicode(s[2:]) + if err != nil { + return r, b, true, err + } + if r1 = utf16.DecodeRune(r1, r2); r1 != unicode.ReplacementChar { + s = s[2+n2:] + } + } + } + + r = appendRune(r, r1) + continue + + default: // not sure what this escape sequence is + return r, b, false, syntaxError(s, "invalid character '%c' in string escape code", c) + } + + r = append(r, c) + s = s[1:] + } + + return r, b, true, nil +} + +func appendRune(b []byte, r rune) []byte { + n := len(b) + b = append(b, 0, 0, 0, 0) + return b[:n+utf8.EncodeRune(b[n:], r)] +} + +func appendCoerceInvalidUTF8(b []byte, s []byte) []byte { + c := [4]byte{} + + for _, r := range string(s) { + b = append(b, c[:utf8.EncodeRune(c[:], r)]...) + } + + return b +} + +func parseObject(b []byte) ([]byte, []byte, error) { + if len(b) < 2 { + return nil, b[len(b):], unexpectedEOF(b) + } + + if b[0] != '{' { + return nil, b, syntaxError(b, "expected '{' at the beginning of an object value") + } + + var err error + var a = b + var n = len(b) + var i = 0 + + b = b[1:] + for { + b = skipSpaces(b) + + if len(b) == 0 { + return nil, b, syntaxError(b, "cannot decode object from empty input") + } + + if b[0] == '}' { + j := (n - len(b)) + 1 + return a[:j], a[j:], nil + } + + if i != 0 { + if len(b) == 0 { + return nil, b, syntaxError(b, "unexpected EOF after object field value") + } + if b[0] != ',' { + return nil, b, syntaxError(b, "expected ',' after object field value but found '%c'", b[0]) + } + b = skipSpaces(b[1:]) + if len(b) == 0 { + return nil, b, unexpectedEOF(b) + } + if b[0] == '}' { + return nil, b, syntaxError(b, "unexpected trailing comma after object field") + } + } + + _, b, err = parseString(b) + if err != nil { + return nil, b, err + } + b = skipSpaces(b) + + if len(b) == 0 { + return nil, b, syntaxError(b, "unexpected EOF after object field key") + } + if b[0] != ':' { + return nil, b, syntaxError(b, "expected ':' after object field key but found '%c'", b[0]) + } + b = skipSpaces(b[1:]) + + _, b, err = parseValue(b) + if err != nil { + return nil, b, err + } + + i++ + } +} + +func parseArray(b []byte) ([]byte, []byte, error) { + if len(b) < 2 { + return nil, b[len(b):], unexpectedEOF(b) + } + + if b[0] != '[' { + return nil, b, syntaxError(b, "expected '[' at the beginning of array value") + } + + var err error + var a = b + var n = len(b) + var i = 0 + + b = b[1:] + for { + b = skipSpaces(b) + + if len(b) == 0 { + return nil, b, syntaxError(b, "missing closing ']' after array value") + } + + if b[0] == ']' { + j := (n - len(b)) + 1 + return a[:j], a[j:], nil + } + + if i != 0 { + if len(b) == 0 { + return nil, b, syntaxError(b, "unexpected EOF after array element") + } + if b[0] != ',' { + return nil, b, syntaxError(b, "expected ',' after array element but found '%c'", b[0]) + } + b = skipSpaces(b[1:]) + if len(b) == 0 { + return nil, b, unexpectedEOF(b) + } + if b[0] == ']' { + return nil, b, syntaxError(b, "unexpected trailing comma after object field") + } + } + + _, b, err = parseValue(b) + if err != nil { + return nil, b, err + } + + i++ + } +} + +func parseValue(b []byte) ([]byte, []byte, error) { + if len(b) != 0 { + switch b[0] { + case '{': + return parseObject(b) + case '[': + return parseArray(b) + case '"': + return parseString(b) + case 'n': + return parseNull(b) + case 't': + return parseTrue(b) + case 'f': + return parseFalse(b) + case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + return parseNumber(b) + default: + return nil, b, syntaxError(b, "invalid character '%c' looking for beginning of value", b[0]) + } + } + return nil, b, syntaxError(b, "unexpected end of JSON input") +} + +func hasNullPrefix(b []byte) bool { + return len(b) >= 4 && string(b[:4]) == "null" +} + +func hasTruePrefix(b []byte) bool { + return len(b) >= 4 && string(b[:4]) == "true" +} + +func hasFalsePrefix(b []byte) bool { + return len(b) >= 5 && string(b[:5]) == "false" +} + +func hasPrefix(b []byte, s string) bool { + return len(b) >= len(s) && s == string(b[:len(s)]) +} + +func hasLeadingSign(b []byte) bool { + return len(b) > 0 && (b[0] == '+' || b[0] == '-') +} + +func hasLeadingZeroes(b []byte) bool { + if hasLeadingSign(b) { + b = b[1:] + } + return len(b) > 1 && b[0] == '0' && '0' <= b[1] && b[1] <= '9' +} + +func appendToLower(b, s []byte) []byte { + if ascii.Valid(s) { // fast path for ascii strings + i := 0 + + for j := range s { + c := s[j] + + if 'A' <= c && c <= 'Z' { + b = append(b, s[i:j]...) + b = append(b, c+('a'-'A')) + i = j + 1 + } + } + + return append(b, s[i:]...) + } + + for _, r := range string(s) { + b = appendRune(b, foldRune(r)) + } + + return b +} + +func foldRune(r rune) rune { + if r = unicode.SimpleFold(r); 'A' <= r && r <= 'Z' { + r = r + ('a' - 'A') + } + return r +} diff --git a/utils/encoding/json/parse_test.go b/utils/encoding/json/parse_test.go new file mode 100644 index 00000000..7fd1b53a --- /dev/null +++ b/utils/encoding/json/parse_test.go @@ -0,0 +1,174 @@ +package json + +import ( + "bytes" + "strings" + "testing" +) + +func TestParseString(t *testing.T) { + tests := []struct { + in string + out string + ext string + }{ + {`""`, `""`, ``}, + {`"1234567890"`, `"1234567890"`, ``}, + {`"Hello World!"`, `"Hello World!"`, ``}, + {`"Hello\"World!"`, `"Hello\"World!"`, ``}, + {`"\\"`, `"\\"`, ``}, + } + + for _, test := range tests { + t.Run(test.in, func(t *testing.T) { + out, ext, err := parseString([]byte(test.in)) + + if err != nil { + t.Errorf("%s => %s", test.in, err) + return + } + + if s := string(out); s != test.out { + t.Error("invalid output") + t.Logf("expected: %s", test.out) + t.Logf("found: %s", s) + } + + if s := string(ext); s != test.ext { + t.Error("invalid extra bytes") + t.Logf("expected: %s", test.ext) + t.Logf("found: %s", s) + } + }) + } +} + +func TestAppendToLower(t *testing.T) { + tests := []string{ + "", + "A", + "a", + "__segment_internal", + "someFieldWithALongBName", + "Hello World!", + "Hello\"World!", + "Hello\\World!", + "Hello\nWorld!", + "Hello\rWorld!", + "Hello\tWorld!", + "Hello\bWorld!", + "Hello\fWorld!", + "ไฝ ๅฅฝ", + "<", + ">", + "&", + "\u001944", + "\u00c2e>", + "\u00c2V?", + "\u000e=8", + "\u001944\u00c2e>\u00c2V?\u000e=8", + "ir\u001bQJ\u007f\u0007y\u0015)", + } + + for _, test := range tests { + s1 := strings.ToLower(test) + s2 := string(appendToLower(nil, []byte(test))) + + if s1 != s2 { + t.Error("lowercase values mismatch") + t.Log("expected:", s1) + t.Log("found: ", s2) + } + } +} + +func BenchmarkParseString(b *testing.B) { + s := []byte(`"__segment_internal"`) + + for i := 0; i != b.N; i++ { + parseString(s) + } +} + +func BenchmarkToLower(b *testing.B) { + s := []byte("someFieldWithALongName") + + for i := 0; i != b.N; i++ { + bytes.ToLower(s) + } +} + +func BenchmarkAppendToLower(b *testing.B) { + a := []byte(nil) + s := []byte("someFieldWithALongName") + + for i := 0; i != b.N; i++ { + a = appendToLower(a[:0], s) + } +} + +var benchmarkHasPrefixString = []byte("some random string") +var benchmarkHasPrefixResult = false + +func BenchmarkHasPrefix(b *testing.B) { + for i := 0; i < b.N; i++ { + benchmarkHasPrefixResult = hasPrefix(benchmarkHasPrefixString, "null") + } +} + +func BenchmarkHasNullPrefix(b *testing.B) { + for i := 0; i < b.N; i++ { + benchmarkHasPrefixResult = hasNullPrefix(benchmarkHasPrefixString) + } +} + +func BenchmarkHasTruePrefix(b *testing.B) { + for i := 0; i < b.N; i++ { + benchmarkHasPrefixResult = hasTruePrefix(benchmarkHasPrefixString) + } +} + +func BenchmarkHasFalsePrefix(b *testing.B) { + for i := 0; i < b.N; i++ { + benchmarkHasPrefixResult = hasFalsePrefix(benchmarkHasPrefixString) + } +} + +func BenchmarkParseStringEscapeNone(b *testing.B) { + var j = []byte(`"` + strings.Repeat(`a`, 1000) + `"`) + var s string + b.SetBytes(int64(len(j))) + + for i := 0; i < b.N; i++ { + if err := Unmarshal(j, &s); err != nil { + b.Fatal(err) + } + s = "" + } +} + +func BenchmarkParseStringEscapeOne(b *testing.B) { + var j = []byte(`"` + strings.Repeat(`a`, 998) + `\n"`) + var s string + b.SetBytes(int64(len(j))) + + for i := 0; i < b.N; i++ { + if err := Unmarshal(j, &s); err != nil { + b.Fatal(err) + } + s = "" + } +} + +func BenchmarkParseStringEscapeAll(b *testing.B) { + var j = []byte(`"` + strings.Repeat(`\`, 1000) + `"`) + var s string + b.SetBytes(int64(len(j))) + + for i := 0; i < b.N; i++ { + if err := Unmarshal(j, &s); err != nil { + b.Fatal(err) + } + s = "" + } +} diff --git a/utils/encoding/json/reflect.go b/utils/encoding/json/reflect.go new file mode 100644 index 00000000..3f852f1c --- /dev/null +++ b/utils/encoding/json/reflect.go @@ -0,0 +1,19 @@ +// +build go1.15 + +package json + +import ( + "reflect" + "unsafe" +) + +func extendSlice(t reflect.Type, s *slice, n int) slice { + arrayType := reflect.ArrayOf(n, t.Elem()) + arrayData := reflect.New(arrayType) + reflect.Copy(arrayData.Elem(), reflect.NewAt(t, unsafe.Pointer(s)).Elem()) + return slice{ + data: unsafe.Pointer(arrayData.Pointer()), + len: s.len, + cap: n, + } +} diff --git a/utils/encoding/json/reflect_optimize.go b/utils/encoding/json/reflect_optimize.go new file mode 100644 index 00000000..ab8fc9ee --- /dev/null +++ b/utils/encoding/json/reflect_optimize.go @@ -0,0 +1,29 @@ +// +build !go1.15 + +package json + +import ( + "reflect" + "unsafe" +) + +//go:linkname unsafe_NewArray reflect.unsafe_NewArray +func unsafe_NewArray(rtype unsafe.Pointer, length int) unsafe.Pointer + +//go:linkname typedslicecopy reflect.typedslicecopy +//go:noescape +func typedslicecopy(elemType unsafe.Pointer, dst, src slice) int + +func extendSlice(t reflect.Type, s *slice, n int) slice { + elemTypeRef := t.Elem() + elemTypePtr := ((*iface)(unsafe.Pointer(&elemTypeRef))).ptr + + d := slice{ + data: unsafe_NewArray(elemTypePtr, n), + len: s.len, + cap: n, + } + + typedslicecopy(elemTypePtr, d, *s) + return d +} diff --git a/utils/encoding/json/testdata/code.json.gz b/utils/encoding/json/testdata/code.json.gz new file mode 100644 index 00000000..028c3f9d Binary files /dev/null and b/utils/encoding/json/testdata/code.json.gz differ diff --git a/utils/encoding/json/testdata/msgs.json.gz b/utils/encoding/json/testdata/msgs.json.gz new file mode 100644 index 00000000..e5c87dac Binary files /dev/null and b/utils/encoding/json/testdata/msgs.json.gz differ diff --git a/utils/encoding/json/token.go b/utils/encoding/json/token.go new file mode 100644 index 00000000..b7994018 --- /dev/null +++ b/utils/encoding/json/token.go @@ -0,0 +1,286 @@ +package json + +// Tokenizer is an iterator-style type which can be used to progressively parse +// through a json input. +// +// Tokenizing json is useful to build highly efficient parsing operations, for +// example when doing tranformations on-the-fly where as the program reads the +// input and produces the transformed json to an output buffer. +// +// Here is a common pattern to use a tokenizer: +// +// for t := json.NewTokenizer(b); t.Next(); { +// switch t.Delim { +// case '{': +// ... +// case '}': +// ... +// case '[': +// ... +// case ']': +// ... +// case ':': +// ... +// case ',': +// ... +// } +// +// switch { +// case t.Value.String(): +// ... +// case t.Value.Null(): +// ... +// case t.Value.True(): +// ... +// case t.Value.False(): +// ... +// case t.Value.Number(): +// ... +// } +// } +// +type Tokenizer struct { + // When the tokenizer is positioned on a json delimiter this field is not + // zero. In this case the possible values are '{', '}', '[', ']', ':', and + // ','. + Delim Delim + + // This field contains the raw json token that the tokenizer is pointing at. + // When Delim is not zero, this field is a single-element byte slice + // continaing the delimiter value. Otherwise, this field holds values like + // null, true, false, numbers, or quoted strings. + Value RawValue + + // When the tokenizer has encountered invalid content this field is not nil. + Err error + + // When the value is in an array or an object, this field contains the depth + // at which it was found. + Depth int + + // When the value is in an array or an object, this field contains the + // position at which it was found. + Index int + + // This field is true when the value is the key of an object. + IsKey bool + + // Tells whether the next value read from the tokenizer is a key. + isKey bool + + // json input for the tokenizer, pointing at data right after the last token + // that was parsed. + json []byte + + // Stack used to track entering and leaving arrays, objects, and keys. The + // buffer is used as a pre-allocated space to + stack []state + buffer [8]state +} + +type state struct { + typ scope + len int +} + +type scope int + +const ( + inArray scope = iota + inObject +) + +// NewTokenizer constructs a new Tokenizer which reads its json input from b. +func NewTokenizer(b []byte) *Tokenizer { return &Tokenizer{json: b} } + +// Reset erases the state of t and re-initializes it with the json input from b. +func (t *Tokenizer) Reset(b []byte) { + // This code is similar to: + // + // *t = Tokenizer{json: b} + // + // However, it does not compile down to an invocation of duff-copy, which + // ends up being slower and prevents the code from being inlined. + t.Delim = 0 + t.Value = nil + t.Err = nil + t.Depth = 0 + t.Index = 0 + t.IsKey = false + t.isKey = false + t.json = b + t.stack = nil +} + +// Next returns a new tokenizer pointing at the next token, or the zero-value of +// Tokenizer if the end of the json input has been reached. +// +// If the tokenizer encounters malformed json while reading the input the method +// sets t.Err to an error describing the issue, and returns false. Once an error +// has been encountered, the tokenizer will always fail until its input is +// cleared by a call to its Reset method. +func (t *Tokenizer) Next() bool { + if t.Err != nil { + return false + } + + // Inlined code of the skipSpaces function, this give a ~15% speed boost. + i := 0 +skipLoop: + for _, c := range t.json { + switch c { + case sp, ht, nl, cr: + i++ + default: + break skipLoop + } + } + + if t.json = t.json[i:]; len(t.json) == 0 { + t.Reset(nil) + return false + } + + var d Delim + var v []byte + var b []byte + var err error + + switch t.json[0] { + case '"': + v, b, err = parseString(t.json) + case 'n': + v, b, err = parseNull(t.json) + case 't': + v, b, err = parseTrue(t.json) + case 'f': + v, b, err = parseFalse(t.json) + case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + v, b, err = parseNumber(t.json) + case '{', '}', '[', ']', ':', ',': + d, v, b = Delim(t.json[0]), t.json[:1], t.json[1:] + default: + v, b, err = t.json[:1], t.json[1:], syntaxError(t.json, "expected token but found '%c'", t.json[0]) + } + + t.Delim = d + t.Value = RawValue(v) + t.Err = err + t.Depth = t.depth() + t.Index = t.index() + t.IsKey = d == 0 && t.isKey + t.json = b + + if d != 0 { + switch d { + case '{': + t.isKey = true + t.push(inObject) + case '[': + t.push(inArray) + case '}': + err = t.pop(inObject) + t.Depth-- + t.Index = t.index() + case ']': + err = t.pop(inArray) + t.Depth-- + t.Index = t.index() + case ':': + t.isKey = false + case ',': + if t.is(inObject) { + t.isKey = true + } + t.stack[len(t.stack)-1].len++ + } + } + + return (d != 0 || len(v) != 0) && err == nil +} + +func (t *Tokenizer) push(typ scope) { + if t.stack == nil { + t.stack = t.buffer[:0] + } + t.stack = append(t.stack, state{typ: typ, len: 1}) +} + +func (t *Tokenizer) pop(expect scope) error { + i := len(t.stack) - 1 + + if i < 0 { + return syntaxError(t.json, "found unexpected character while tokenizing json input") + } + + if found := t.stack[i]; expect != found.typ { + return syntaxError(t.json, "found unexpected character while tokenizing json input") + } + + t.stack = t.stack[:i] + return nil +} + +func (t *Tokenizer) is(typ scope) bool { + return len(t.stack) != 0 && t.stack[len(t.stack)-1].typ == typ +} + +func (t *Tokenizer) depth() int { + return len(t.stack) +} + +func (t *Tokenizer) index() int { + if len(t.stack) == 0 { + return 0 + } + return t.stack[len(t.stack)-1].len - 1 +} + +// RawValue represents a raw json value, it is intended to carry null, true, +// false, number, and string values only. +type RawValue []byte + +// String returns true if v contains a string value. +func (v RawValue) String() bool { return len(v) != 0 && v[0] == '"' } + +// Null returns true if v contains a null value. +func (v RawValue) Null() bool { return len(v) != 0 && v[0] == 'n' } + +// True returns true if v contains a true value. +func (v RawValue) True() bool { return len(v) != 0 && v[0] == 't' } + +// False returns true if v contains a false value. +func (v RawValue) False() bool { return len(v) != 0 && v[0] == 'f' } + +// Number returns true if v contains a number value. +func (v RawValue) Number() bool { + if len(v) != 0 { + switch v[0] { + case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + return true + } + } + return false +} + +// AppendUnquote writes the unquoted version of the string value in v into b. +func (v RawValue) AppendUnquote(b []byte) []byte { + s, r, new, err := parseStringUnquote([]byte(v), b) + if err != nil { + panic(err) + } + if len(r) != 0 { + panic(syntaxError(r, "unexpected trailing tokens after json value")) + } + if new { + b = s + } else { + b = append(b, s...) + } + return b +} + +// Unquote returns the unquoted version of the string value in v. +func (v RawValue) Unquote() []byte { + return v.AppendUnquote(nil) +} diff --git a/utils/encoding/json/token_test.go b/utils/encoding/json/token_test.go new file mode 100644 index 00000000..69759a33 --- /dev/null +++ b/utils/encoding/json/token_test.go @@ -0,0 +1,287 @@ +package json + +import ( + "reflect" + "testing" +) + +type token struct { + delim Delim + value RawValue + err error + depth int + index int + isKey bool +} + +func delim(s string, depth, index int) token { + return token{ + delim: Delim(s[0]), + value: RawValue(s), + depth: depth, + index: index, + } +} + +func key(v string, depth, index int) token { + return token{ + value: RawValue(v), + depth: depth, + index: index, + isKey: true, + } +} + +func value(v string, depth, index int) token { + return token{ + value: RawValue(v), + depth: depth, + index: index, + } +} + +func tokenize(b []byte) (tokens []token) { + t := NewTokenizer(b) + + for t.Next() { + tokens = append(tokens, token{ + delim: t.Delim, + value: t.Value, + err: t.Err, + depth: t.Depth, + index: t.Index, + isKey: t.IsKey, + }) + } + + if t.Err != nil { + panic(t.Err) + } + + return +} + +func TestTokenizer(t *testing.T) { + tests := []struct { + input []byte + tokens []token + }{ + { + input: []byte(`null`), + tokens: []token{ + value(`null`, 0, 0), + }, + }, + + { + input: []byte(`true`), + tokens: []token{ + value(`true`, 0, 0), + }, + }, + + { + input: []byte(`false`), + tokens: []token{ + value(`false`, 0, 0), + }, + }, + + { + input: []byte(`""`), + tokens: []token{ + value(`""`, 0, 0), + }, + }, + + { + input: []byte(`"Hello World!"`), + tokens: []token{ + value(`"Hello World!"`, 0, 0), + }, + }, + + { + input: []byte(`-0.1234`), + tokens: []token{ + value(`-0.1234`, 0, 0), + }, + }, + + { + input: []byte(` { } `), + tokens: []token{ + delim(`{`, 0, 0), + delim(`}`, 0, 0), + }, + }, + + { + input: []byte(`{ "answer": 42 }`), + tokens: []token{ + delim(`{`, 0, 0), + key(`"answer"`, 1, 0), + delim(`:`, 1, 0), + value(`42`, 1, 0), + delim(`}`, 0, 0), + }, + }, + + { + input: []byte(`{ "sub": { "key-A": 1, "key-B": 2, "key-C": 3 } }`), + tokens: []token{ + delim(`{`, 0, 0), + key(`"sub"`, 1, 0), + delim(`:`, 1, 0), + delim(`{`, 1, 0), + key(`"key-A"`, 2, 0), + delim(`:`, 2, 0), + value(`1`, 2, 0), + delim(`,`, 2, 0), + key(`"key-B"`, 2, 1), + delim(`:`, 2, 1), + value(`2`, 2, 1), + delim(`,`, 2, 1), + key(`"key-C"`, 2, 2), + delim(`:`, 2, 2), + value(`3`, 2, 2), + delim(`}`, 1, 0), + delim(`}`, 0, 0), + }, + }, + + { + input: []byte(` [ ] `), + tokens: []token{ + delim(`[`, 0, 0), + delim(`]`, 0, 0), + }, + }, + + { + input: []byte(`[1, 2, 3]`), + tokens: []token{ + delim(`[`, 0, 0), + value(`1`, 1, 0), + delim(`,`, 1, 0), + value(`2`, 1, 1), + delim(`,`, 1, 1), + value(`3`, 1, 2), + delim(`]`, 0, 0), + }, + }, + } + + for _, test := range tests { + t.Run(string(test.input), func(t *testing.T) { + tokens := tokenize(test.input) + + if !reflect.DeepEqual(tokens, test.tokens) { + t.Error("tokens mismatch") + t.Logf("expected: %+v", test.tokens) + t.Logf("found: %+v", tokens) + } + }) + } +} + +func BenchmarkTokenizer(b *testing.B) { + values := []struct { + scenario string + payload []byte + }{ + { + scenario: "null", + payload: []byte(`null`), + }, + + { + scenario: "true", + payload: []byte(`true`), + }, + + { + scenario: "false", + payload: []byte(`false`), + }, + + { + scenario: "number", + payload: []byte(`-1.23456789`), + }, + + { + scenario: "string", + payload: []byte(`"1234567890"`), + }, + + { + scenario: "object", + payload: []byte(`{ + "timestamp": "2019-01-09T18:59:57.456Z", + "channel": "server", + "type": "track", + "event": "Test", + "userId": "test-user-whatever", + "messageId": "test-message-whatever", + "integrations": { + "whatever": { + "debugMode": false + }, + "myIntegration": { + "debugMode": true + } + }, + "properties": { + "trait1": 1, + "trait2": "test", + "trait3": true + }, + "settings": { + "apiKey": "1234567890", + "debugMode": false, + "directChannels": [ + "server", + "client" + ], + "endpoint": "https://somewhere.com/v1/integrations/segment" + } +}`), + }, + } + + benchmarks := []struct { + scenario string + function func(*testing.B, []byte) + }{ + { + scenario: "github.com/segmentio/encoding/json", + function: func(b *testing.B, json []byte) { + t := NewTokenizer(nil) + + for i := 0; i < b.N; i++ { + t.Reset(json) + + for t.Next() { + // Does nothing other than iterating over each token to measure the + // CPU and memory footprint. + } + + if t.Err != nil { + b.Error(t.Err) + } + } + }, + }, + } + + for _, bechmark := range benchmarks { + b.Run(bechmark.scenario, func(b *testing.B) { + for _, value := range values { + b.Run(value.scenario, func(b *testing.B) { + bechmark.function(b, value.payload) + }) + } + }) + } +} diff --git a/utils/fasttemplate/LICENSE b/utils/fasttemplate/LICENSE new file mode 100644 index 00000000..7125a63c --- /dev/null +++ b/utils/fasttemplate/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Aliaksandr Valialkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/utils/fasttemplate/template.go b/utils/fasttemplate/template.go new file mode 100644 index 00000000..c04c3d95 --- /dev/null +++ b/utils/fasttemplate/template.go @@ -0,0 +1,437 @@ +// Package fasttemplate implements simple and fast template library. +// +// Fasttemplate is faster than text/template, strings.Replace +// and strings.Replacer. +// +// Fasttemplate ideally fits for fast and simple placeholders' substitutions. +package fasttemplate + +import ( + "bytes" + "fmt" + "io" + + "github.com/gofiber/fiber/v2/utils/bytebufferpool" +) + +// ExecuteFunc calls f on each template tag (placeholder) occurrence. +// +// Returns the number of bytes written to w. +// +// This function is optimized for constantly changing templates. +// Use Template.ExecuteFunc for frozen templates. +func ExecuteFunc(template, startTag, endTag string, w io.Writer, f TagFunc) (int64, error) { + s := unsafeString2Bytes(template) + a := unsafeString2Bytes(startTag) + b := unsafeString2Bytes(endTag) + + var nn int64 + var ni int + var err error + for { + n := bytes.Index(s, a) + if n < 0 { + break + } + ni, err = w.Write(s[:n]) + nn += int64(ni) + if err != nil { + return nn, err + } + + s = s[n+len(a):] + n = bytes.Index(s, b) + if n < 0 { + // cannot find end tag - just write it to the output. + ni, _ = w.Write(a) + nn += int64(ni) + break + } + + ni, err = f(w, unsafeBytes2String(s[:n])) + nn += int64(ni) + if err != nil { + return nn, err + } + s = s[n+len(b):] + } + ni, err = w.Write(s) + nn += int64(ni) + + return nn, err +} + +// Execute substitutes template tags (placeholders) with the corresponding +// values from the map m and writes the result to the given writer w. +// +// Substitution map m may contain values with the following types: +// * []byte - the fastest value type +// * string - convenient value type +// * TagFunc - flexible value type +// +// Returns the number of bytes written to w. +// +// This function is optimized for constantly changing templates. +// Use Template.Execute for frozen templates. +func Execute(template, startTag, endTag string, w io.Writer, m map[string]interface{}) (int64, error) { + return ExecuteFunc(template, startTag, endTag, w, func(w io.Writer, tag string) (int, error) { return stdTagFunc(w, tag, m) }) +} + +// ExecuteStd works the same way as Execute, but keeps the unknown placeholders. +// This can be used as a drop-in replacement for strings.Replacer +// +// Substitution map m may contain values with the following types: +// * []byte - the fastest value type +// * string - convenient value type +// * TagFunc - flexible value type +// +// Returns the number of bytes written to w. +// +// This function is optimized for constantly changing templates. +// Use Template.ExecuteStd for frozen templates. +func ExecuteStd(template, startTag, endTag string, w io.Writer, m map[string]interface{}) (int64, error) { + return ExecuteFunc(template, startTag, endTag, w, func(w io.Writer, tag string) (int, error) { return keepUnknownTagFunc(w, startTag, endTag, tag, m) }) +} + +// ExecuteFuncString calls f on each template tag (placeholder) occurrence +// and substitutes it with the data written to TagFunc's w. +// +// Returns the resulting string. +// +// This function is optimized for constantly changing templates. +// Use Template.ExecuteFuncString for frozen templates. +func ExecuteFuncString(template, startTag, endTag string, f TagFunc) string { + s, err := ExecuteFuncStringWithErr(template, startTag, endTag, f) + if err != nil { + panic(fmt.Sprintf("unexpected error: %s", err)) + } + return s +} + +// ExecuteFuncStringWithErr is nearly the same as ExecuteFuncString +// but when f returns an error, ExecuteFuncStringWithErr won't panic like ExecuteFuncString +// it just returns an empty string and the error f returned +func ExecuteFuncStringWithErr(template, startTag, endTag string, f TagFunc) (string, error) { + tagsCount := bytes.Count(unsafeString2Bytes(template), unsafeString2Bytes(startTag)) + if tagsCount == 0 { + return template, nil + } + + bb := byteBufferPool.Get() + if _, err := ExecuteFunc(template, startTag, endTag, bb, f); err != nil { + bb.Reset() + byteBufferPool.Put(bb) + return "", err + } + s := string(bb.B) + bb.Reset() + byteBufferPool.Put(bb) + return s, nil +} + +var byteBufferPool bytebufferpool.Pool + +// ExecuteString substitutes template tags (placeholders) with the corresponding +// values from the map m and returns the result. +// +// Substitution map m may contain values with the following types: +// * []byte - the fastest value type +// * string - convenient value type +// * TagFunc - flexible value type +// +// This function is optimized for constantly changing templates. +// Use Template.ExecuteString for frozen templates. +func ExecuteString(template, startTag, endTag string, m map[string]interface{}) string { + return ExecuteFuncString(template, startTag, endTag, func(w io.Writer, tag string) (int, error) { return stdTagFunc(w, tag, m) }) +} + +// ExecuteStringStd works the same way as ExecuteString, but keeps the unknown placeholders. +// This can be used as a drop-in replacement for strings.Replacer +// +// Substitution map m may contain values with the following types: +// * []byte - the fastest value type +// * string - convenient value type +// * TagFunc - flexible value type +// +// This function is optimized for constantly changing templates. +// Use Template.ExecuteStringStd for frozen templates. +func ExecuteStringStd(template, startTag, endTag string, m map[string]interface{}) string { + return ExecuteFuncString(template, startTag, endTag, func(w io.Writer, tag string) (int, error) { return keepUnknownTagFunc(w, startTag, endTag, tag, m) }) +} + +// Template implements simple template engine, which can be used for fast +// tags' (aka placeholders) substitution. +type Template struct { + template string + startTag string + endTag string + + texts [][]byte + tags []string + byteBufferPool bytebufferpool.Pool +} + +// New parses the given template using the given startTag and endTag +// as tag start and tag end. +// +// The returned template can be executed by concurrently running goroutines +// using Execute* methods. +// +// New panics if the given template cannot be parsed. Use NewTemplate instead +// if template may contain errors. +func New(template, startTag, endTag string) *Template { + t, err := NewTemplate(template, startTag, endTag) + if err != nil { + panic(err) + } + return t +} + +// NewTemplate parses the given template using the given startTag and endTag +// as tag start and tag end. +// +// The returned template can be executed by concurrently running goroutines +// using Execute* methods. +func NewTemplate(template, startTag, endTag string) (*Template, error) { + var t Template + err := t.Reset(template, startTag, endTag) + if err != nil { + return nil, err + } + return &t, nil +} + +// TagFunc can be used as a substitution value in the map passed to Execute*. +// Execute* functions pass tag (placeholder) name in 'tag' argument. +// +// TagFunc must be safe to call from concurrently running goroutines. +// +// TagFunc must write contents to w and return the number of bytes written. +type TagFunc func(w io.Writer, tag string) (int, error) + +// Reset resets the template t to new one defined by +// template, startTag and endTag. +// +// Reset allows Template object re-use. +// +// Reset may be called only if no other goroutines call t methods at the moment. +func (t *Template) Reset(template, startTag, endTag string) error { + // Keep these vars in t, so GC won't collect them and won't break + // vars derived via unsafe* + t.template = template + t.startTag = startTag + t.endTag = endTag + t.texts = t.texts[:0] + t.tags = t.tags[:0] + + if len(startTag) == 0 { + panic("startTag cannot be empty") + } + if len(endTag) == 0 { + panic("endTag cannot be empty") + } + + s := unsafeString2Bytes(template) + a := unsafeString2Bytes(startTag) + b := unsafeString2Bytes(endTag) + + tagsCount := bytes.Count(s, a) + if tagsCount == 0 { + return nil + } + + if tagsCount+1 > cap(t.texts) { + t.texts = make([][]byte, 0, tagsCount+1) + } + if tagsCount > cap(t.tags) { + t.tags = make([]string, 0, tagsCount) + } + + for { + n := bytes.Index(s, a) + if n < 0 { + t.texts = append(t.texts, s) + break + } + t.texts = append(t.texts, s[:n]) + + s = s[n+len(a):] + n = bytes.Index(s, b) + if n < 0 { + return fmt.Errorf("Cannot find end tag=%q in the template=%q starting from %q", endTag, template, s) + } + + t.tags = append(t.tags, unsafeBytes2String(s[:n])) + s = s[n+len(b):] + } + + return nil +} + +// ExecuteFunc calls f on each template tag (placeholder) occurrence. +// +// Returns the number of bytes written to w. +// +// This function is optimized for frozen templates. +// Use ExecuteFunc for constantly changing templates. +func (t *Template) ExecuteFunc(w io.Writer, f TagFunc) (int64, error) { + var nn int64 + + n := len(t.texts) - 1 + if n == -1 { + ni, err := w.Write(unsafeString2Bytes(t.template)) + return int64(ni), err + } + + for i := 0; i < n; i++ { + ni, err := w.Write(t.texts[i]) + nn += int64(ni) + if err != nil { + return nn, err + } + + ni, err = f(w, t.tags[i]) + nn += int64(ni) + if err != nil { + return nn, err + } + } + ni, err := w.Write(t.texts[n]) + nn += int64(ni) + return nn, err +} + +// Execute substitutes template tags (placeholders) with the corresponding +// values from the map m and writes the result to the given writer w. +// +// Substitution map m may contain values with the following types: +// * []byte - the fastest value type +// * string - convenient value type +// * TagFunc - flexible value type +// +// Returns the number of bytes written to w. +func (t *Template) Execute(w io.Writer, m map[string]interface{}) (int64, error) { + return t.ExecuteFunc(w, func(w io.Writer, tag string) (int, error) { return stdTagFunc(w, tag, m) }) +} + +// ExecuteStd works the same way as Execute, but keeps the unknown placeholders. +// This can be used as a drop-in replacement for strings.Replacer +// +// Substitution map m may contain values with the following types: +// * []byte - the fastest value type +// * string - convenient value type +// * TagFunc - flexible value type +// +// Returns the number of bytes written to w. +func (t *Template) ExecuteStd(w io.Writer, m map[string]interface{}) (int64, error) { + return t.ExecuteFunc(w, func(w io.Writer, tag string) (int, error) { return keepUnknownTagFunc(w, t.startTag, t.endTag, tag, m) }) +} + +// ExecuteFuncString calls f on each template tag (placeholder) occurrence +// and substitutes it with the data written to TagFunc's w. +// +// Returns the resulting string. +// +// This function is optimized for frozen templates. +// Use ExecuteFuncString for constantly changing templates. +func (t *Template) ExecuteFuncString(f TagFunc) string { + s, err := t.ExecuteFuncStringWithErr(f) + if err != nil { + panic(fmt.Sprintf("unexpected error: %s", err)) + } + return s +} + +// ExecuteFuncStringWithErr calls f on each template tag (placeholder) occurrence +// and substitutes it with the data written to TagFunc's w. +// +// Returns the resulting string. +// +// This function is optimized for frozen templates. +// Use ExecuteFuncString for constantly changing templates. +func (t *Template) ExecuteFuncStringWithErr(f TagFunc) (string, error) { + bb := t.byteBufferPool.Get() + if _, err := t.ExecuteFunc(bb, f); err != nil { + bb.Reset() + t.byteBufferPool.Put(bb) + return "", err + } + s := string(bb.Bytes()) + bb.Reset() + t.byteBufferPool.Put(bb) + return s, nil +} + +// ExecuteString substitutes template tags (placeholders) with the corresponding +// values from the map m and returns the result. +// +// Substitution map m may contain values with the following types: +// * []byte - the fastest value type +// * string - convenient value type +// * TagFunc - flexible value type +// +// This function is optimized for frozen templates. +// Use ExecuteString for constantly changing templates. +func (t *Template) ExecuteString(m map[string]interface{}) string { + return t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) { return stdTagFunc(w, tag, m) }) +} + +// ExecuteStringStd works the same way as ExecuteString, but keeps the unknown placeholders. +// This can be used as a drop-in replacement for strings.Replacer +// +// Substitution map m may contain values with the following types: +// * []byte - the fastest value type +// * string - convenient value type +// * TagFunc - flexible value type +// +// This function is optimized for frozen templates. +// Use ExecuteStringStd for constantly changing templates. +func (t *Template) ExecuteStringStd(m map[string]interface{}) string { + return t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) { return keepUnknownTagFunc(w, t.startTag, t.endTag, tag, m) }) +} + +func stdTagFunc(w io.Writer, tag string, m map[string]interface{}) (int, error) { + v := m[tag] + if v == nil { + return 0, nil + } + switch value := v.(type) { + case []byte: + return w.Write(value) + case string: + return w.Write([]byte(value)) + case TagFunc: + return value(w, tag) + default: + panic(fmt.Sprintf("tag=%q contains unexpected value type=%#v. Expected []byte, string or TagFunc", tag, v)) + } +} + +func keepUnknownTagFunc(w io.Writer, startTag, endTag, tag string, m map[string]interface{}) (int, error) { + v, ok := m[tag] + if !ok { + if _, err := w.Write(unsafeString2Bytes(startTag)); err != nil { + return 0, err + } + if _, err := w.Write(unsafeString2Bytes(tag)); err != nil { + return 0, err + } + if _, err := w.Write(unsafeString2Bytes(endTag)); err != nil { + return 0, err + } + return len(startTag) + len(tag) + len(endTag), nil + } + if v == nil { + return 0, nil + } + switch value := v.(type) { + case []byte: + return w.Write(value) + case string: + return w.Write([]byte(value)) + case TagFunc: + return value(w, tag) + default: + panic(fmt.Sprintf("tag=%q contains unexpected value type=%#v. Expected []byte, string or TagFunc", tag, v)) + } +} diff --git a/utils/fasttemplate/template_test.go b/utils/fasttemplate/template_test.go new file mode 100644 index 00000000..24c7e89f --- /dev/null +++ b/utils/fasttemplate/template_test.go @@ -0,0 +1,436 @@ +package fasttemplate + +import ( + "bytes" + "errors" + "io" + "testing" +) + +func TestEmptyTemplate(t *testing.T) { + tpl := New("", "[", "]") + + s := tpl.ExecuteString(map[string]interface{}{"foo": "bar", "aaa": "bbb"}) + if s != "" { + t.Fatalf("unexpected string returned %q. Expected empty string", s) + } +} + +func TestEmptyTagStart(t *testing.T) { + expectPanic(t, func() { NewTemplate("foobar", "", "]") }) +} + +func TestEmptyTagEnd(t *testing.T) { + expectPanic(t, func() { NewTemplate("foobar", "[", "") }) +} + +func TestNoTags(t *testing.T) { + template := "foobar" + tpl := New(template, "[", "]") + + s := tpl.ExecuteString(map[string]interface{}{"foo": "bar", "aaa": "bbb"}) + if s != template { + t.Fatalf("unexpected template value %q. Expected %q", s, template) + } +} + +func TestEmptyTagName(t *testing.T) { + template := "foo[]bar" + tpl := New(template, "[", "]") + + s := tpl.ExecuteString(map[string]interface{}{"": "111", "aaa": "bbb"}) + result := "foo111bar" + if s != result { + t.Fatalf("unexpected template value %q. Expected %q", s, result) + } +} + +func TestOnlyTag(t *testing.T) { + template := "[foo]" + tpl := New(template, "[", "]") + + s := tpl.ExecuteString(map[string]interface{}{"foo": "111", "aaa": "bbb"}) + result := "111" + if s != result { + t.Fatalf("unexpected template value %q. Expected %q", s, result) + } +} + +func TestStartWithTag(t *testing.T) { + template := "[foo]barbaz" + tpl := New(template, "[", "]") + + s := tpl.ExecuteString(map[string]interface{}{"foo": "111", "aaa": "bbb"}) + result := "111barbaz" + if s != result { + t.Fatalf("unexpected template value %q. Expected %q", s, result) + } +} + +func TestEndWithTag(t *testing.T) { + template := "foobar[foo]" + tpl := New(template, "[", "]") + + s := tpl.ExecuteString(map[string]interface{}{"foo": "111", "aaa": "bbb"}) + result := "foobar111" + if s != result { + t.Fatalf("unexpected template value %q. Expected %q", s, result) + } +} + +func TestTemplateReset(t *testing.T) { + template := "foo{bar}baz" + tpl := New(template, "{", "}") + s := tpl.ExecuteString(map[string]interface{}{"bar": "111"}) + result := "foo111baz" + if s != result { + t.Fatalf("unexpected template value %q. Expected %q", s, result) + } + + template = "[xxxyyyzz" + if err := tpl.Reset(template, "[", "]"); err == nil { + t.Fatalf("expecting error for unclosed tag on %q", template) + } + + template = "[xxx]yyy[zz]" + if err := tpl.Reset(template, "[", "]"); err != nil { + t.Fatalf("unexpected error: %s", err) + } + s = tpl.ExecuteString(map[string]interface{}{"xxx": "11", "zz": "2222"}) + result = "11yyy2222" + if s != result { + t.Fatalf("unexpected template value %q. Expected %q", s, result) + } +} + +func TestDuplicateTags(t *testing.T) { + template := "[foo]bar[foo][foo]baz" + tpl := New(template, "[", "]") + + s := tpl.ExecuteString(map[string]interface{}{"foo": "111", "aaa": "bbb"}) + result := "111bar111111baz" + if s != result { + t.Fatalf("unexpected template value %q. Expected %q", s, result) + } +} + +func TestMultipleTags(t *testing.T) { + template := "foo[foo]aa[aaa]ccc" + tpl := New(template, "[", "]") + + s := tpl.ExecuteString(map[string]interface{}{"foo": "111", "aaa": "bbb"}) + result := "foo111aabbbccc" + if s != result { + t.Fatalf("unexpected template value %q. Expected %q", s, result) + } +} + +func TestLongDelimiter(t *testing.T) { + template := "foo{{{foo}}}bar" + tpl := New(template, "{{{", "}}}") + + s := tpl.ExecuteString(map[string]interface{}{"foo": "111", "aaa": "bbb"}) + result := "foo111bar" + if s != result { + t.Fatalf("unexpected template value %q. Expected %q", s, result) + } +} + +func TestIdenticalDelimiter(t *testing.T) { + template := "foo@foo@foo@aaa@" + tpl := New(template, "@", "@") + + s := tpl.ExecuteString(map[string]interface{}{"foo": "111", "aaa": "bbb"}) + result := "foo111foobbb" + if s != result { + t.Fatalf("unexpected template value %q. Expected %q", s, result) + } +} + +func TestDlimitersWithDistinctSize(t *testing.T) { + template := "foobar" + tpl := New(template, "") + + s := tpl.ExecuteString(map[string]interface{}{"zzz": "111", "aaa": "bbb"}) + result := "foobbbbar111" + if s != result { + t.Fatalf("unexpected template value %q. Expected %q", s, result) + } +} + +func TestEmptyValue(t *testing.T) { + template := "foobar[foo]" + tpl := New(template, "[", "]") + + s := tpl.ExecuteString(map[string]interface{}{"foo": "", "aaa": "bbb"}) + result := "foobar" + if s != result { + t.Fatalf("unexpected template value %q. Expected %q", s, result) + } +} + +func TestNoValue(t *testing.T) { + template := "foobar[foo]x[aaa]" + tpl := New(template, "[", "]") + + s := tpl.ExecuteString(map[string]interface{}{"aaa": "bbb"}) + result := "foobarxbbb" + if s != result { + t.Fatalf("unexpected template value %q. Expected %q", s, result) + } +} + +func TestNoEndDelimiter(t *testing.T) { + template := "foobar[foo" + _, err := NewTemplate(template, "[", "]") + if err == nil { + t.Fatalf("expected non-nil error. got nil") + } + + expectPanic(t, func() { New(template, "[", "]") }) +} + +func TestUnsupportedValue(t *testing.T) { + template := "foobar[foo]" + tpl := New(template, "[", "]") + + expectPanic(t, func() { + tpl.ExecuteString(map[string]interface{}{"foo": 123, "aaa": "bbb"}) + }) +} + +func TestMixedValues(t *testing.T) { + template := "foo[foo]bar[bar]baz[baz]" + tpl := New(template, "[", "]") + + s := tpl.ExecuteString(map[string]interface{}{ + "foo": "111", + "bar": []byte("bbb"), + "baz": TagFunc(func(w io.Writer, tag string) (int, error) { return w.Write([]byte(tag)) }), + }) + result := "foo111barbbbbazbaz" + if s != result { + t.Fatalf("unexpected template value %q. Expected %q", s, result) + } +} + +func TestExecuteFunc(t *testing.T) { + testExecuteFunc(t, "", "") + testExecuteFunc(t, "a", "a") + testExecuteFunc(t, "abc", "abc") + testExecuteFunc(t, "{foo}", "xxxx") + testExecuteFunc(t, "a{foo}", "axxxx") + testExecuteFunc(t, "{foo}a", "xxxxa") + testExecuteFunc(t, "a{foo}bc", "axxxxbc") + testExecuteFunc(t, "{foo}{foo}", "xxxxxxxx") + testExecuteFunc(t, "{foo}bar{foo}", "xxxxbarxxxx") + + // unclosed tag + testExecuteFunc(t, "{unclosed", "{unclosed") + testExecuteFunc(t, "{{unclosed", "{{unclosed") + testExecuteFunc(t, "{un{closed", "{un{closed") + + // test unknown tag + testExecuteFunc(t, "{unknown}", "zz") + testExecuteFunc(t, "{foo}q{unexpected}{missing}bar{foo}", "xxxxqzzzzbarxxxx") +} + +func testExecuteFunc(t *testing.T, template, expectedOutput string) { + var bb bytes.Buffer + ExecuteFunc(template, "{", "}", &bb, func(w io.Writer, tag string) (int, error) { + if tag == "foo" { + return w.Write([]byte("xxxx")) + } + return w.Write([]byte("zz")) + }) + + output := string(bb.Bytes()) + if output != expectedOutput { + t.Fatalf("unexpected output for template=%q: %q. Expected %q", template, output, expectedOutput) + } +} + +func TestExecute(t *testing.T) { + testExecute(t, "", "") + testExecute(t, "a", "a") + testExecute(t, "abc", "abc") + testExecute(t, "{foo}", "xxxx") + testExecute(t, "a{foo}", "axxxx") + testExecute(t, "{foo}a", "xxxxa") + testExecute(t, "a{foo}bc", "axxxxbc") + testExecute(t, "{foo}{foo}", "xxxxxxxx") + testExecute(t, "{foo}bar{foo}", "xxxxbarxxxx") + + // unclosed tag + testExecute(t, "{unclosed", "{unclosed") + testExecute(t, "{{unclosed", "{{unclosed") + testExecute(t, "{un{closed", "{un{closed") + + // test unknown tag + testExecute(t, "{unknown}", "") + testExecute(t, "{foo}q{unexpected}{missing}bar{foo}", "xxxxqbarxxxx") +} + +func testExecute(t *testing.T, template, expectedOutput string) { + var bb bytes.Buffer + Execute(template, "{", "}", &bb, map[string]interface{}{"foo": "xxxx"}) + output := string(bb.Bytes()) + if output != expectedOutput { + t.Fatalf("unexpected output for template=%q: %q. Expected %q", template, output, expectedOutput) + } +} + +func TestExecuteStd(t *testing.T) { + testExecuteStd(t, "", "") + testExecuteStd(t, "a", "a") + testExecuteStd(t, "abc", "abc") + testExecuteStd(t, "{foo}", "xxxx") + testExecuteStd(t, "a{foo}", "axxxx") + testExecuteStd(t, "{foo}a", "xxxxa") + testExecuteStd(t, "a{foo}bc", "axxxxbc") + testExecuteStd(t, "{foo}{foo}", "xxxxxxxx") + testExecuteStd(t, "{foo}bar{foo}", "xxxxbarxxxx") + + // unclosed tag + testExecuteStd(t, "{unclosed", "{unclosed") + testExecuteStd(t, "{{unclosed", "{{unclosed") + testExecuteStd(t, "{un{closed", "{un{closed") + + // test unknown tag + testExecuteStd(t, "{unknown}", "{unknown}") + testExecuteStd(t, "{foo}q{unexpected}{missing}bar{foo}", "xxxxq{unexpected}{missing}barxxxx") +} + +func testExecuteStd(t *testing.T, template, expectedOutput string) { + var bb bytes.Buffer + ExecuteStd(template, "{", "}", &bb, map[string]interface{}{"foo": "xxxx"}) + output := string(bb.Bytes()) + if output != expectedOutput { + t.Fatalf("unexpected output for template=%q: %q. Expected %q", template, output, expectedOutput) + } +} + +func TestExecuteString(t *testing.T) { + testExecuteString(t, "", "") + testExecuteString(t, "a", "a") + testExecuteString(t, "abc", "abc") + testExecuteString(t, "{foo}", "xxxx") + testExecuteString(t, "a{foo}", "axxxx") + testExecuteString(t, "{foo}a", "xxxxa") + testExecuteString(t, "a{foo}bc", "axxxxbc") + testExecuteString(t, "{foo}{foo}", "xxxxxxxx") + testExecuteString(t, "{foo}bar{foo}", "xxxxbarxxxx") + + // unclosed tag + testExecuteString(t, "{unclosed", "{unclosed") + testExecuteString(t, "{{unclosed", "{{unclosed") + testExecuteString(t, "{un{closed", "{un{closed") + + // test unknown tag + testExecuteString(t, "{unknown}", "") + testExecuteString(t, "{foo}q{unexpected}{missing}bar{foo}", "xxxxqbarxxxx") +} + +func testExecuteString(t *testing.T, template, expectedOutput string) { + output := ExecuteString(template, "{", "}", map[string]interface{}{"foo": "xxxx"}) + if output != expectedOutput { + t.Fatalf("unexpected output for template=%q: %q. Expected %q", template, output, expectedOutput) + } +} + +func TestExecuteStringStd(t *testing.T) { + testExecuteStringStd(t, "", "") + testExecuteStringStd(t, "a", "a") + testExecuteStringStd(t, "abc", "abc") + testExecuteStringStd(t, "{foo}", "xxxx") + testExecuteStringStd(t, "a{foo}", "axxxx") + testExecuteStringStd(t, "{foo}a", "xxxxa") + testExecuteStringStd(t, "a{foo}bc", "axxxxbc") + testExecuteStringStd(t, "{foo}{foo}", "xxxxxxxx") + testExecuteStringStd(t, "{foo}bar{foo}", "xxxxbarxxxx") + + // unclosed tag + testExecuteStringStd(t, "{unclosed", "{unclosed") + testExecuteStringStd(t, "{{unclosed", "{{unclosed") + testExecuteStringStd(t, "{un{closed", "{un{closed") + + // test unknown tag + testExecuteStringStd(t, "{unknown}", "{unknown}") + testExecuteStringStd(t, "{foo}q{unexpected}{missing}bar{foo}", "xxxxq{unexpected}{missing}barxxxx") +} + +func testExecuteStringStd(t *testing.T, template, expectedOutput string) { + output := ExecuteStringStd(template, "{", "}", map[string]interface{}{"foo": "xxxx"}) + if output != expectedOutput { + t.Fatalf("unexpected output for template=%q: %q. Expected %q", template, output, expectedOutput) + } +} + +func expectPanic(t *testing.T, f func()) { + defer func() { + if r := recover(); r == nil { + t.Fatalf("missing panic") + } + }() + f() +} + +func TestExecuteFuncStringWithErr(t *testing.T) { + var expectErr = errors.New("test111") + result, err := ExecuteFuncStringWithErr(`{a} is {b}'s best friend`, "{", "}", func(w io.Writer, tag string) (int, error) { + if tag == "a" { + return w.Write([]byte("Alice")) + } + return 0, expectErr + }) + if err != expectErr { + t.Fatalf("error must be the same as the error returned from f, expect: %s, actual: %s", expectErr, err) + } + if result != "" { + t.Fatalf("result should be an empty string if error occurred") + } + result, err = ExecuteFuncStringWithErr(`{a} is {b}'s best friend`, "{", "}", func(w io.Writer, tag string) (int, error) { + if tag == "a" { + return w.Write([]byte("Alice")) + } + return w.Write([]byte("Bob")) + }) + if err != nil { + t.Fatalf("should success but found err: %s", err) + } + if result != "Alice is Bob's best friend" { + t.Fatalf("expect: %s, but: %s", "Alice is Bob's best friend", result) + } +} + +func TestTpl_ExecuteFuncStringWithErr(t *testing.T) { + var expectErr = errors.New("test111") + tpl, err := NewTemplate(`{a} is {b}'s best friend`, "{", "}") + if err != nil { + t.Fatalf("should success, but found err: %s", err) + } + result, err := tpl.ExecuteFuncStringWithErr(func(w io.Writer, tag string) (int, error) { + if tag == "a" { + return w.Write([]byte("Alice")) + } + return 0, expectErr + }) + if err != expectErr { + t.Fatalf("error must be the same as the error returned from f, expect: %s, actual: %s", expectErr, err) + } + if result != "" { + t.Fatalf("result should be an empty string if error occurred") + } + result, err = tpl.ExecuteFuncStringWithErr(func(w io.Writer, tag string) (int, error) { + if tag == "a" { + return w.Write([]byte("Alice")) + } + return w.Write([]byte("Bob")) + }) + if err != nil { + t.Fatalf("should success but found err: %s", err) + } + if result != "Alice is Bob's best friend" { + t.Fatalf("expect: %s, but: %s", "Alice is Bob's best friend", result) + } +} diff --git a/utils/fasttemplate/template_timing_test.go b/utils/fasttemplate/template_timing_test.go new file mode 100644 index 00000000..96dd8b0a --- /dev/null +++ b/utils/fasttemplate/template_timing_test.go @@ -0,0 +1,312 @@ +package fasttemplate + +import ( + "bytes" + "fmt" + "io" + "net/url" + "strings" + "testing" + "text/template" +) + +var ( + source = "http://{{uid}}.foo.bar.com/?cb={{cb}}{{width}}&width={{width}}&height={{height}}&timeout={{timeout}}&uid={{uid}}&subid={{subid}}&ref={{ref}}&empty={{empty}}" + result = "http://aaasdf.foo.bar.com/?cb=12341232&width=1232&height=123&timeout=123123&uid=aaasdf&subid=asdfds&ref=http://google.com/aaa/bbb/ccc&empty=" + resultEscaped = "http://aaasdf.foo.bar.com/?cb=12341232&width=1232&height=123&timeout=123123&uid=aaasdf&subid=asdfds&ref=http%3A%2F%2Fgoogle.com%2Faaa%2Fbbb%2Fccc&empty=" + resultStd = "http://aaasdf.foo.bar.com/?cb=12341232&width=1232&height=123&timeout=123123&uid=aaasdf&subid=asdfds&ref=http://google.com/aaa/bbb/ccc&empty={{empty}}" + resultTextTemplate = "http://aaasdf.foo.bar.com/?cb=12341232&width=1232&height=123&timeout=123123&uid=aaasdf&subid=asdfds&ref=http://google.com/aaa/bbb/ccc&empty=" + + resultBytes = []byte(result) + resultEscapedBytes = []byte(resultEscaped) + resultStdBytes = []byte(resultStd) + resultTextTemplateBytes = []byte(resultTextTemplate) + + m = map[string]interface{}{ + "cb": []byte("1234"), + "width": []byte("1232"), + "height": []byte("123"), + "timeout": []byte("123123"), + "uid": []byte("aaasdf"), + "subid": []byte("asdfds"), + "ref": []byte("http://google.com/aaa/bbb/ccc"), + } +) + +func map2slice(m map[string]interface{}) []string { + var a []string + for k, v := range m { + a = append(a, "{{"+k+"}}", string(v.([]byte))) + } + return a +} + +func BenchmarkFmtFprintf(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + var w bytes.Buffer + for pb.Next() { + fmt.Fprintf(&w, + "http://%[5]s.foo.bar.com/?cb=%[1]s%[2]s&width=%[2]s&height=%[3]s&timeout=%[4]s&uid=%[5]s&subid=%[6]s&ref=%[7]s&empty=", + m["cb"], m["width"], m["height"], m["timeout"], m["uid"], m["subid"], m["ref"]) + x := w.Bytes() + if !bytes.Equal(x, resultBytes) { + b.Fatalf("Unexpected result\n%q\nExpected\n%q\n", x, result) + } + w.Reset() + } + }) +} + +func BenchmarkStringsReplace(b *testing.B) { + mSlice := map2slice(m) + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + x := source + for i := 0; i < len(mSlice); i += 2 { + x = strings.Replace(x, mSlice[i], mSlice[i+1], -1) + } + if x != resultStd { + b.Fatalf("Unexpected result\n%q\nExpected\n%q\n", x, resultStd) + } + } + }) +} + +func BenchmarkStringsReplacer(b *testing.B) { + mSlice := map2slice(m) + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + r := strings.NewReplacer(mSlice...) + x := r.Replace(source) + if x != resultStd { + b.Fatalf("Unexpected result\n%q\nExpected\n%q\n", x, resultStd) + } + } + }) +} + +func BenchmarkTextTemplate(b *testing.B) { + s := strings.Replace(source, "{{", "{{.", -1) + t, err := template.New("test").Parse(s) + if err != nil { + b.Fatalf("Error when parsing template: %s", err) + } + + mm := make(map[string]string) + for k, v := range m { + mm[k] = string(v.([]byte)) + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + var w bytes.Buffer + for pb.Next() { + if err := t.Execute(&w, mm); err != nil { + b.Fatalf("error when executing template: %s", err) + } + x := w.Bytes() + if !bytes.Equal(x, resultTextTemplateBytes) { + b.Fatalf("unexpected result\n%q\nExpected\n%q\n", x, resultTextTemplateBytes) + } + w.Reset() + } + }) +} + +func BenchmarkFastTemplateExecuteFunc(b *testing.B) { + t, err := NewTemplate(source, "{{", "}}") + if err != nil { + b.Fatalf("error in template: %s", err) + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + var w bytes.Buffer + for pb.Next() { + if _, err := t.ExecuteFunc(&w, testTagFunc); err != nil { + b.Fatalf("unexpected error: %s", err) + } + x := w.Bytes() + if !bytes.Equal(x, resultBytes) { + b.Fatalf("unexpected result\n%q\nExpected\n%q\n", x, resultBytes) + } + w.Reset() + } + }) +} + +func BenchmarkFastTemplateExecute(b *testing.B) { + t, err := NewTemplate(source, "{{", "}}") + if err != nil { + b.Fatalf("error in template: %s", err) + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + var w bytes.Buffer + for pb.Next() { + if _, err := t.Execute(&w, m); err != nil { + b.Fatalf("unexpected error: %s", err) + } + x := w.Bytes() + if !bytes.Equal(x, resultBytes) { + b.Fatalf("unexpected result\n%q\nExpected\n%q\n", x, resultBytes) + } + w.Reset() + } + }) +} + +func BenchmarkFastTemplateExecuteStd(b *testing.B) { + t, err := NewTemplate(source, "{{", "}}") + if err != nil { + b.Fatalf("error in template: %s", err) + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + var w bytes.Buffer + for pb.Next() { + if _, err := t.ExecuteStd(&w, m); err != nil { + b.Fatalf("unexpected error: %s", err) + } + x := w.Bytes() + if !bytes.Equal(x, resultStdBytes) { + b.Fatalf("unexpected result\n%q\nExpected\n%q\n", x, resultStdBytes) + } + w.Reset() + } + }) +} + +func BenchmarkFastTemplateExecuteFuncString(b *testing.B) { + t, err := NewTemplate(source, "{{", "}}") + if err != nil { + b.Fatalf("error in template: %s", err) + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + x := t.ExecuteFuncString(testTagFunc) + if x != result { + b.Fatalf("unexpected result\n%q\nExpected\n%q\n", x, result) + } + } + }) +} + +func BenchmarkFastTemplateExecuteString(b *testing.B) { + t, err := NewTemplate(source, "{{", "}}") + if err != nil { + b.Fatalf("error in template: %s", err) + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + x := t.ExecuteString(m) + if x != result { + b.Fatalf("unexpected result\n%q\nExpected\n%q\n", x, result) + } + } + }) +} + +func BenchmarkFastTemplateExecuteStringStd(b *testing.B) { + t, err := NewTemplate(source, "{{", "}}") + if err != nil { + b.Fatalf("error in template: %s", err) + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + x := t.ExecuteStringStd(m) + if x != resultStd { + b.Fatalf("unexpected result\n%q\nExpected\n%q\n", x, resultStd) + } + } + }) +} + +func BenchmarkFastTemplateExecuteTagFunc(b *testing.B) { + t, err := NewTemplate(source, "{{", "}}") + if err != nil { + b.Fatalf("error in template: %s", err) + } + + mm := make(map[string]interface{}) + for k, v := range m { + if k == "ref" { + vv := v.([]byte) + v = TagFunc(func(w io.Writer, tag string) (int, error) { return w.Write([]byte(url.QueryEscape(string(vv)))) }) + } + mm[k] = v + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + var w bytes.Buffer + for pb.Next() { + if _, err := t.Execute(&w, mm); err != nil { + b.Fatalf("unexpected error: %s", err) + } + x := w.Bytes() + if !bytes.Equal(x, resultEscapedBytes) { + b.Fatalf("unexpected result\n%q\nExpected\n%q\n", x, resultEscapedBytes) + } + w.Reset() + } + }) +} + +func BenchmarkNewTemplate(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _ = New(source, "{{", "}}") + } + }) +} + +func BenchmarkTemplateReset(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + t := New(source, "{{", "}}") + for pb.Next() { + t.Reset(source, "{{", "}}") + } + }) +} + +func BenchmarkTemplateResetExecuteFunc(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + t := New(source, "{{", "}}") + var w bytes.Buffer + for pb.Next() { + t.Reset(source, "{{", "}}") + t.ExecuteFunc(&w, testTagFunc) + w.Reset() + } + }) +} + +func BenchmarkExecuteFunc(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + var bb bytes.Buffer + for pb.Next() { + ExecuteFunc(source, "{{", "}}", &bb, testTagFunc) + bb.Reset() + } + }) +} + +func testTagFunc(w io.Writer, tag string) (int, error) { + if t, ok := m[tag]; ok { + return w.Write(t.([]byte)) + } + return 0, nil +} diff --git a/utils/fasttemplate/unsafe.go b/utils/fasttemplate/unsafe.go new file mode 100644 index 00000000..1020ca38 --- /dev/null +++ b/utils/fasttemplate/unsafe.go @@ -0,0 +1,21 @@ +// +build !appengine + +package fasttemplate + +import ( + "reflect" + "unsafe" +) + +func unsafeBytes2String(b []byte) string { + return *(*string)(unsafe.Pointer(&b)) +} + +func unsafeString2Bytes(s string) (b []byte) { + sh := (*reflect.StringHeader)(unsafe.Pointer(&s)) + bh := (*reflect.SliceHeader)(unsafe.Pointer(&b)) + bh.Data = sh.Data + bh.Cap = sh.Len + bh.Len = sh.Len + return b +} diff --git a/utils/fasttemplate/unsafe_gae.go b/utils/fasttemplate/unsafe_gae.go new file mode 100644 index 00000000..cc4ce151 --- /dev/null +++ b/utils/fasttemplate/unsafe_gae.go @@ -0,0 +1,11 @@ +// +build appengine + +package fasttemplate + +func unsafeBytes2String(b []byte) string { + return string(b) +} + +func unsafeString2Bytes(s string) []byte { + return []byte(s) +} diff --git a/utils/isatty/LICENSE b/utils/isatty/LICENSE new file mode 100644 index 00000000..65dc692b --- /dev/null +++ b/utils/isatty/LICENSE @@ -0,0 +1,9 @@ +Copyright (c) Yasuhiro MATSUMOTO + +MIT License (Expat) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/utils/isatty/isatty_bsd.go b/utils/isatty/isatty_bsd.go new file mode 100644 index 00000000..711f2880 --- /dev/null +++ b/utils/isatty/isatty_bsd.go @@ -0,0 +1,18 @@ +// +build darwin freebsd openbsd netbsd dragonfly +// +build !appengine + +package isatty + +import "golang.org/x/sys/unix" + +// IsTerminal return true if the file descriptor is terminal. +func IsTerminal(fd uintptr) bool { + _, err := unix.IoctlGetTermios(int(fd), unix.TIOCGETA) + return err == nil +} + +// IsCygwinTerminal return true if the file descriptor is a cygwin or msys2 +// terminal. This is also always false on this environment. +func IsCygwinTerminal(fd uintptr) bool { + return false +} diff --git a/utils/isatty/isatty_others.go b/utils/isatty/isatty_others.go new file mode 100644 index 00000000..3eba4cb3 --- /dev/null +++ b/utils/isatty/isatty_others.go @@ -0,0 +1,15 @@ +// +build appengine js nacl wasm + +package isatty + +// IsTerminal returns true if the file descriptor is terminal which +// is always false on js and appengine classic which is a sandboxed PaaS. +func IsTerminal(fd uintptr) bool { + return false +} + +// IsCygwinTerminal() return true if the file descriptor is a cygwin or msys2 +// terminal. This is also always false on this environment. +func IsCygwinTerminal(fd uintptr) bool { + return false +} diff --git a/utils/isatty/isatty_others_test.go b/utils/isatty/isatty_others_test.go new file mode 100644 index 00000000..a2091cf4 --- /dev/null +++ b/utils/isatty/isatty_others_test.go @@ -0,0 +1,19 @@ +// +build !windows + +package isatty + +import ( + "os" + "testing" +) + +func TestTerminal(t *testing.T) { + // test for non-panic + IsTerminal(os.Stdout.Fd()) +} + +func TestCygwinPipeName(t *testing.T) { + if IsCygwinTerminal(os.Stdout.Fd()) { + t.Fatal("should be false always") + } +} diff --git a/utils/isatty/isatty_plan9.go b/utils/isatty/isatty_plan9.go new file mode 100644 index 00000000..c5b6e0c0 --- /dev/null +++ b/utils/isatty/isatty_plan9.go @@ -0,0 +1,22 @@ +// +build plan9 + +package isatty + +import ( + "syscall" +) + +// IsTerminal returns true if the given file descriptor is a terminal. +func IsTerminal(fd uintptr) bool { + path, err := syscall.Fd2path(int(fd)) + if err != nil { + return false + } + return path == "/dev/cons" || path == "/mnt/term/dev/cons" +} + +// IsCygwinTerminal return true if the file descriptor is a cygwin or msys2 +// terminal. This is also always false on this environment. +func IsCygwinTerminal(fd uintptr) bool { + return false +} diff --git a/utils/isatty/isatty_solaris.go b/utils/isatty/isatty_solaris.go new file mode 100644 index 00000000..bdd5c79a --- /dev/null +++ b/utils/isatty/isatty_solaris.go @@ -0,0 +1,22 @@ +// +build solaris +// +build !appengine + +package isatty + +import ( + "golang.org/x/sys/unix" +) + +// IsTerminal returns true if the given file descriptor is a terminal. +// see: http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libbc/libc/gen/common/isatty.c +func IsTerminal(fd uintptr) bool { + var termio unix.Termio + err := unix.IoctlSetTermio(int(fd), unix.TCGETA, &termio) + return err == nil +} + +// IsCygwinTerminal return true if the file descriptor is a cygwin or msys2 +// terminal. This is also always false on this environment. +func IsCygwinTerminal(fd uintptr) bool { + return false +} diff --git a/utils/isatty/isatty_tcgets.go b/utils/isatty/isatty_tcgets.go new file mode 100644 index 00000000..31a1ca97 --- /dev/null +++ b/utils/isatty/isatty_tcgets.go @@ -0,0 +1,18 @@ +// +build linux aix +// +build !appengine + +package isatty + +import "golang.org/x/sys/unix" + +// IsTerminal return true if the file descriptor is terminal. +func IsTerminal(fd uintptr) bool { + _, err := unix.IoctlGetTermios(int(fd), unix.TCGETS) + return err == nil +} + +// IsCygwinTerminal return true if the file descriptor is a cygwin or msys2 +// terminal. This is also always false on this environment. +func IsCygwinTerminal(fd uintptr) bool { + return false +} diff --git a/utils/isatty/isatty_windows.go b/utils/isatty/isatty_windows.go new file mode 100644 index 00000000..1fa86915 --- /dev/null +++ b/utils/isatty/isatty_windows.go @@ -0,0 +1,125 @@ +// +build windows +// +build !appengine + +package isatty + +import ( + "errors" + "strings" + "syscall" + "unicode/utf16" + "unsafe" +) + +const ( + objectNameInfo uintptr = 1 + fileNameInfo = 2 + fileTypePipe = 3 +) + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + ntdll = syscall.NewLazyDLL("ntdll.dll") + procGetConsoleMode = kernel32.NewProc("GetConsoleMode") + procGetFileInformationByHandleEx = kernel32.NewProc("GetFileInformationByHandleEx") + procGetFileType = kernel32.NewProc("GetFileType") + procNtQueryObject = ntdll.NewProc("NtQueryObject") +) + +func init() { + // Check if GetFileInformationByHandleEx is available. + if procGetFileInformationByHandleEx.Find() != nil { + procGetFileInformationByHandleEx = nil + } +} + +// IsTerminal return true if the file descriptor is terminal. +func IsTerminal(fd uintptr) bool { + var st uint32 + r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, fd, uintptr(unsafe.Pointer(&st)), 0) + return r != 0 && e == 0 +} + +// Check pipe name is used for cygwin/msys2 pty. +// Cygwin/MSYS2 PTY has a name like: +// \{cygwin,msys}-XXXXXXXXXXXXXXXX-ptyN-{from,to}-master +func isCygwinPipeName(name string) bool { + token := strings.Split(name, "-") + if len(token) < 5 { + return false + } + + if token[0] != `\msys` && + token[0] != `\cygwin` && + token[0] != `\Device\NamedPipe\msys` && + token[0] != `\Device\NamedPipe\cygwin` { + return false + } + + if token[1] == "" { + return false + } + + if !strings.HasPrefix(token[2], "pty") { + return false + } + + if token[3] != `from` && token[3] != `to` { + return false + } + + if token[4] != "master" { + return false + } + + return true +} + +// getFileNameByHandle use the undocomented ntdll NtQueryObject to get file full name from file handler +// since GetFileInformationByHandleEx is not avilable under windows Vista and still some old fashion +// guys are using Windows XP, this is a workaround for those guys, it will also work on system from +// Windows vista to 10 +// see https://stackoverflow.com/a/18792477 for details +func getFileNameByHandle(fd uintptr) (string, error) { + if procNtQueryObject == nil { + return "", errors.New("ntdll.dll: NtQueryObject not supported") + } + + var buf [4 + syscall.MAX_PATH]uint16 + var result int + r, _, e := syscall.Syscall6(procNtQueryObject.Addr(), 5, + fd, objectNameInfo, uintptr(unsafe.Pointer(&buf)), uintptr(2*len(buf)), uintptr(unsafe.Pointer(&result)), 0) + if r != 0 { + return "", e + } + return string(utf16.Decode(buf[4 : 4+buf[0]/2])), nil +} + +// IsCygwinTerminal() return true if the file descriptor is a cygwin or msys2 +// terminal. +func IsCygwinTerminal(fd uintptr) bool { + if procGetFileInformationByHandleEx == nil { + name, err := getFileNameByHandle(fd) + if err != nil { + return false + } + return isCygwinPipeName(name) + } + + // Cygwin/msys's pty is a pipe. + ft, _, e := syscall.Syscall(procGetFileType.Addr(), 1, fd, 0, 0) + if ft != fileTypePipe || e != 0 { + return false + } + + var buf [2 + syscall.MAX_PATH]uint16 + r, _, e := syscall.Syscall6(procGetFileInformationByHandleEx.Addr(), + 4, fd, fileNameInfo, uintptr(unsafe.Pointer(&buf)), + uintptr(len(buf)*2), 0, 0) + if r == 0 || e != 0 { + return false + } + + l := *(*uint32)(unsafe.Pointer(&buf)) + return isCygwinPipeName(string(utf16.Decode(buf[2 : 2+l/2]))) +} diff --git a/utils/isatty/isatty_windows_test.go b/utils/isatty/isatty_windows_test.go new file mode 100644 index 00000000..5a68c1f0 --- /dev/null +++ b/utils/isatty/isatty_windows_test.go @@ -0,0 +1,38 @@ +// +build windows + +package isatty + +import ( + "testing" +) + +func TestCygwinPipeName(t *testing.T) { + tests := []struct { + name string + result bool + }{ + {``, false}, + {`\msys-`, false}, + {`\cygwin-----`, false}, + {`\msys-x-PTY5-pty1-from-master`, false}, + {`\cygwin-x-PTY5-from-master`, false}, + {`\cygwin-x-pty2-from-toaster`, false}, + {`\cygwin--pty2-from-master`, false}, + {`\\cygwin-x-pty2-from-master`, false}, + {`\cygwin-x-pty2-from-master-`, true}, // for the feature + {`\cygwin-e022582115c10879-pty4-from-master`, true}, + {`\msys-e022582115c10879-pty4-to-master`, true}, + {`\cygwin-e022582115c10879-pty4-to-master`, true}, + {`\Device\NamedPipe\cygwin-e022582115c10879-pty4-from-master`, true}, + {`\Device\NamedPipe\msys-e022582115c10879-pty4-to-master`, true}, + {`Device\NamedPipe\cygwin-e022582115c10879-pty4-to-master`, false}, + } + + for _, test := range tests { + want := test.result + got := isCygwinPipeName(test.name) + if want != got { + t.Fatalf("isatty(%q): got %v, want %v:", test.name, got, want) + } + } +} diff --git a/utils/schema/LICENSE b/utils/schema/LICENSE new file mode 100644 index 00000000..0e5fb872 --- /dev/null +++ b/utils/schema/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012 Rodrigo Moraes. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/utils/schema/cache.go b/utils/schema/cache.go new file mode 100644 index 00000000..0746c120 --- /dev/null +++ b/utils/schema/cache.go @@ -0,0 +1,305 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package schema + +import ( + "errors" + "reflect" + "strconv" + "strings" + "sync" +) + +var invalidPath = errors.New("schema: invalid path") + +// newCache returns a new cache. +func newCache() *cache { + c := cache{ + m: make(map[reflect.Type]*structInfo), + regconv: make(map[reflect.Type]Converter), + tag: "schema", + } + return &c +} + +// cache caches meta-data about a struct. +type cache struct { + l sync.RWMutex + m map[reflect.Type]*structInfo + regconv map[reflect.Type]Converter + tag string +} + +// registerConverter registers a converter function for a custom type. +func (c *cache) registerConverter(value interface{}, converterFunc Converter) { + c.regconv[reflect.TypeOf(value)] = converterFunc +} + +// parsePath parses a path in dotted notation verifying that it is a valid +// path to a struct field. +// +// It returns "path parts" which contain indices to fields to be used by +// reflect.Value.FieldByString(). Multiple parts are required for slices of +// structs. +func (c *cache) parsePath(p string, t reflect.Type) ([]pathPart, error) { + var struc *structInfo + var field *fieldInfo + var index64 int64 + var err error + parts := make([]pathPart, 0) + path := make([]string, 0) + keys := strings.Split(p, ".") + for i := 0; i < len(keys); i++ { + if t.Kind() != reflect.Struct { + return nil, invalidPath + } + if struc = c.get(t); struc == nil { + return nil, invalidPath + } + if field = struc.get(keys[i]); field == nil { + return nil, invalidPath + } + // Valid field. Append index. + path = append(path, field.name) + if field.isSliceOfStructs && (!field.unmarshalerInfo.IsValid || (field.unmarshalerInfo.IsValid && field.unmarshalerInfo.IsSliceElement)) { + // Parse a special case: slices of structs. + // i+1 must be the slice index. + // + // Now that struct can implements TextUnmarshaler interface, + // we don't need to force the struct's fields to appear in the path. + // So checking i+2 is not necessary anymore. + i++ + if i+1 > len(keys) { + return nil, invalidPath + } + if index64, err = strconv.ParseInt(keys[i], 10, 0); err != nil { + return nil, invalidPath + } + parts = append(parts, pathPart{ + path: path, + field: field, + index: int(index64), + }) + path = make([]string, 0) + + // Get the next struct type, dropping ptrs. + if field.typ.Kind() == reflect.Ptr { + t = field.typ.Elem() + } else { + t = field.typ + } + if t.Kind() == reflect.Slice { + t = t.Elem() + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + } + } else if field.typ.Kind() == reflect.Ptr { + t = field.typ.Elem() + } else { + t = field.typ + } + } + // Add the remaining. + parts = append(parts, pathPart{ + path: path, + field: field, + index: -1, + }) + return parts, nil +} + +// get returns a cached structInfo, creating it if necessary. +func (c *cache) get(t reflect.Type) *structInfo { + c.l.RLock() + info := c.m[t] + c.l.RUnlock() + if info == nil { + info = c.create(t, "") + c.l.Lock() + c.m[t] = info + c.l.Unlock() + } + return info +} + +// create creates a structInfo with meta-data about a struct. +func (c *cache) create(t reflect.Type, parentAlias string) *structInfo { + info := &structInfo{} + var anonymousInfos []*structInfo + for i := 0; i < t.NumField(); i++ { + if f := c.createField(t.Field(i), parentAlias); f != nil { + info.fields = append(info.fields, f) + if ft := indirectType(f.typ); ft.Kind() == reflect.Struct && f.isAnonymous { + anonymousInfos = append(anonymousInfos, c.create(ft, f.canonicalAlias)) + } + } + } + for i, a := range anonymousInfos { + others := []*structInfo{info} + others = append(others, anonymousInfos[:i]...) + others = append(others, anonymousInfos[i+1:]...) + for _, f := range a.fields { + if !containsAlias(others, f.alias) { + info.fields = append(info.fields, f) + } + } + } + return info +} + +// createField creates a fieldInfo for the given field. +func (c *cache) createField(field reflect.StructField, parentAlias string) *fieldInfo { + alias, options := fieldAlias(field, c.tag) + if alias == "-" { + // Ignore this field. + return nil + } + canonicalAlias := alias + if parentAlias != "" { + canonicalAlias = parentAlias + "." + alias + } + // Check if the type is supported and don't cache it if not. + // First let's get the basic type. + isSlice, isStruct := false, false + ft := field.Type + m := isTextUnmarshaler(reflect.Zero(ft)) + if ft.Kind() == reflect.Ptr { + ft = ft.Elem() + } + if isSlice = ft.Kind() == reflect.Slice; isSlice { + ft = ft.Elem() + if ft.Kind() == reflect.Ptr { + ft = ft.Elem() + } + } + if ft.Kind() == reflect.Array { + ft = ft.Elem() + if ft.Kind() == reflect.Ptr { + ft = ft.Elem() + } + } + if isStruct = ft.Kind() == reflect.Struct; !isStruct { + if c.converter(ft) == nil && builtinConverters[ft.Kind()] == nil { + // Type is not supported. + return nil + } + } + + return &fieldInfo{ + typ: field.Type, + name: field.Name, + alias: alias, + canonicalAlias: canonicalAlias, + unmarshalerInfo: m, + isSliceOfStructs: isSlice && isStruct, + isAnonymous: field.Anonymous, + isRequired: options.Contains("required"), + } +} + +// converter returns the converter for a type. +func (c *cache) converter(t reflect.Type) Converter { + return c.regconv[t] +} + +// ---------------------------------------------------------------------------- + +type structInfo struct { + fields []*fieldInfo +} + +func (i *structInfo) get(alias string) *fieldInfo { + for _, field := range i.fields { + if strings.EqualFold(field.alias, alias) { + return field + } + } + return nil +} + +func containsAlias(infos []*structInfo, alias string) bool { + for _, info := range infos { + if info.get(alias) != nil { + return true + } + } + return false +} + +type fieldInfo struct { + typ reflect.Type + // name is the field name in the struct. + name string + alias string + // canonicalAlias is almost the same as the alias, but is prefixed with + // an embedded struct field alias in dotted notation if this field is + // promoted from the struct. + // For instance, if the alias is "N" and this field is an embedded field + // in a struct "X", canonicalAlias will be "X.N". + canonicalAlias string + // unmarshalerInfo contains information regarding the + // encoding.TextUnmarshaler implementation of the field type. + unmarshalerInfo unmarshaler + // isSliceOfStructs indicates if the field type is a slice of structs. + isSliceOfStructs bool + // isAnonymous indicates whether the field is embedded in the struct. + isAnonymous bool + isRequired bool +} + +func (f *fieldInfo) paths(prefix string) []string { + if f.alias == f.canonicalAlias { + return []string{prefix + f.alias} + } + return []string{prefix + f.alias, prefix + f.canonicalAlias} +} + +type pathPart struct { + field *fieldInfo + path []string // path to the field: walks structs using field names. + index int // struct index in slices of structs. +} + +// ---------------------------------------------------------------------------- + +func indirectType(typ reflect.Type) reflect.Type { + if typ.Kind() == reflect.Ptr { + return typ.Elem() + } + return typ +} + +// fieldAlias parses a field tag to get a field alias. +func fieldAlias(field reflect.StructField, tagName string) (alias string, options tagOptions) { + if tag := field.Tag.Get(tagName); tag != "" { + alias, options = parseTag(tag) + } + if alias == "" { + alias = field.Name + } + return alias, options +} + +// tagOptions is the string following a comma in a struct field's tag, or +// the empty string. It does not include the leading comma. +type tagOptions []string + +// parseTag splits a struct field's url tag into its name and comma-separated +// options. +func parseTag(tag string) (string, tagOptions) { + s := strings.Split(tag, ",") + return s[0], s[1:] +} + +// Contains checks whether the tagOptions contains the specified option. +func (o tagOptions) Contains(option string) bool { + for _, s := range o { + if s == option { + return true + } + } + return false +} diff --git a/utils/schema/converter.go b/utils/schema/converter.go new file mode 100644 index 00000000..4f2116a1 --- /dev/null +++ b/utils/schema/converter.go @@ -0,0 +1,145 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package schema + +import ( + "reflect" + "strconv" +) + +type Converter func(string) reflect.Value + +var ( + invalidValue = reflect.Value{} + boolType = reflect.Bool + float32Type = reflect.Float32 + float64Type = reflect.Float64 + intType = reflect.Int + int8Type = reflect.Int8 + int16Type = reflect.Int16 + int32Type = reflect.Int32 + int64Type = reflect.Int64 + stringType = reflect.String + uintType = reflect.Uint + uint8Type = reflect.Uint8 + uint16Type = reflect.Uint16 + uint32Type = reflect.Uint32 + uint64Type = reflect.Uint64 +) + +// Default converters for basic types. +var builtinConverters = map[reflect.Kind]Converter{ + boolType: convertBool, + float32Type: convertFloat32, + float64Type: convertFloat64, + intType: convertInt, + int8Type: convertInt8, + int16Type: convertInt16, + int32Type: convertInt32, + int64Type: convertInt64, + stringType: convertString, + uintType: convertUint, + uint8Type: convertUint8, + uint16Type: convertUint16, + uint32Type: convertUint32, + uint64Type: convertUint64, +} + +func convertBool(value string) reflect.Value { + if value == "on" { + return reflect.ValueOf(true) + } else if v, err := strconv.ParseBool(value); err == nil { + return reflect.ValueOf(v) + } + return invalidValue +} + +func convertFloat32(value string) reflect.Value { + if v, err := strconv.ParseFloat(value, 32); err == nil { + return reflect.ValueOf(float32(v)) + } + return invalidValue +} + +func convertFloat64(value string) reflect.Value { + if v, err := strconv.ParseFloat(value, 64); err == nil { + return reflect.ValueOf(v) + } + return invalidValue +} + +func convertInt(value string) reflect.Value { + if v, err := strconv.ParseInt(value, 10, 0); err == nil { + return reflect.ValueOf(int(v)) + } + return invalidValue +} + +func convertInt8(value string) reflect.Value { + if v, err := strconv.ParseInt(value, 10, 8); err == nil { + return reflect.ValueOf(int8(v)) + } + return invalidValue +} + +func convertInt16(value string) reflect.Value { + if v, err := strconv.ParseInt(value, 10, 16); err == nil { + return reflect.ValueOf(int16(v)) + } + return invalidValue +} + +func convertInt32(value string) reflect.Value { + if v, err := strconv.ParseInt(value, 10, 32); err == nil { + return reflect.ValueOf(int32(v)) + } + return invalidValue +} + +func convertInt64(value string) reflect.Value { + if v, err := strconv.ParseInt(value, 10, 64); err == nil { + return reflect.ValueOf(v) + } + return invalidValue +} + +func convertString(value string) reflect.Value { + return reflect.ValueOf(value) +} + +func convertUint(value string) reflect.Value { + if v, err := strconv.ParseUint(value, 10, 0); err == nil { + return reflect.ValueOf(uint(v)) + } + return invalidValue +} + +func convertUint8(value string) reflect.Value { + if v, err := strconv.ParseUint(value, 10, 8); err == nil { + return reflect.ValueOf(uint8(v)) + } + return invalidValue +} + +func convertUint16(value string) reflect.Value { + if v, err := strconv.ParseUint(value, 10, 16); err == nil { + return reflect.ValueOf(uint16(v)) + } + return invalidValue +} + +func convertUint32(value string) reflect.Value { + if v, err := strconv.ParseUint(value, 10, 32); err == nil { + return reflect.ValueOf(uint32(v)) + } + return invalidValue +} + +func convertUint64(value string) reflect.Value { + if v, err := strconv.ParseUint(value, 10, 64); err == nil { + return reflect.ValueOf(v) + } + return invalidValue +} diff --git a/utils/schema/decoder.go b/utils/schema/decoder.go new file mode 100644 index 00000000..025e438b --- /dev/null +++ b/utils/schema/decoder.go @@ -0,0 +1,521 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package schema + +import ( + "encoding" + "errors" + "fmt" + "reflect" + "strings" +) + +// NewDecoder returns a new Decoder. +func NewDecoder() *Decoder { + return &Decoder{cache: newCache()} +} + +// Decoder decodes values from a map[string][]string to a struct. +type Decoder struct { + cache *cache + zeroEmpty bool + ignoreUnknownKeys bool +} + +// SetAliasTag changes the tag used to locate custom field aliases. +// The default tag is "schema". +func (d *Decoder) SetAliasTag(tag string) { + d.cache.tag = tag +} + +// ZeroEmpty controls the behaviour when the decoder encounters empty values +// in a map. +// If z is true and a key in the map has the empty string as a value +// then the corresponding struct field is set to the zero value. +// If z is false then empty strings are ignored. +// +// The default value is false, that is empty values do not change +// the value of the struct field. +func (d *Decoder) ZeroEmpty(z bool) { + d.zeroEmpty = z +} + +// IgnoreUnknownKeys controls the behaviour when the decoder encounters unknown +// keys in the map. +// If i is true and an unknown field is encountered, it is ignored. This is +// similar to how unknown keys are handled by encoding/json. +// If i is false then Decode will return an error. Note that any valid keys +// will still be decoded in to the target struct. +// +// To preserve backwards compatibility, the default value is false. +func (d *Decoder) IgnoreUnknownKeys(i bool) { + d.ignoreUnknownKeys = i +} + +// RegisterConverter registers a converter function for a custom type. +func (d *Decoder) RegisterConverter(value interface{}, converterFunc Converter) { + d.cache.registerConverter(value, converterFunc) +} + +// Decode decodes a map[string][]string to a struct. +// +// The first parameter must be a pointer to a struct. +// +// The second parameter is a map, typically url.Values from an HTTP request. +// Keys are "paths" in dotted notation to the struct fields and nested structs. +// +// See the package documentation for a full explanation of the mechanics. +func (d *Decoder) Decode(dst interface{}, src map[string][]string) error { + v := reflect.ValueOf(dst) + if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct { + return errors.New("schema: interface must be a pointer to struct") + } + v = v.Elem() + t := v.Type() + errors := MultiError{} + for path, values := range src { + if parts, err := d.cache.parsePath(path, t); err == nil { + if err = d.decode(v, path, parts, values); err != nil { + errors[path] = err + } + } else if !d.ignoreUnknownKeys { + errors[path] = UnknownKeyError{Key: path} + } + } + errors.merge(d.checkRequired(t, src)) + if len(errors) > 0 { + return errors + } + return nil +} + +// checkRequired checks whether required fields are empty +// +// check type t recursively if t has struct fields. +// +// src is the source map for decoding, we use it here to see if those required fields are included in src +func (d *Decoder) checkRequired(t reflect.Type, src map[string][]string) MultiError { + m, errs := d.findRequiredFields(t, "", "") + for key, fields := range m { + if isEmptyFields(fields, src) { + errs[key] = EmptyFieldError{Key: key} + } + } + return errs +} + +// findRequiredFields recursively searches the struct type t for required fields. +// +// canonicalPrefix and searchPrefix are used to resolve full paths in dotted notation +// for nested struct fields. canonicalPrefix is a complete path which never omits +// any embedded struct fields. searchPrefix is a user-friendly path which may omit +// some embedded struct fields to point promoted fields. +func (d *Decoder) findRequiredFields(t reflect.Type, canonicalPrefix, searchPrefix string) (map[string][]fieldWithPrefix, MultiError) { + struc := d.cache.get(t) + if struc == nil { + // unexpect, cache.get never return nil + return nil, MultiError{canonicalPrefix + "*": errors.New("cache fail")} + } + + m := map[string][]fieldWithPrefix{} + errs := MultiError{} + for _, f := range struc.fields { + if f.typ.Kind() == reflect.Struct { + fcprefix := canonicalPrefix + f.canonicalAlias + "." + for _, fspath := range f.paths(searchPrefix) { + fm, ferrs := d.findRequiredFields(f.typ, fcprefix, fspath+".") + for key, fields := range fm { + m[key] = append(m[key], fields...) + } + errs.merge(ferrs) + } + } + if f.isRequired { + key := canonicalPrefix + f.canonicalAlias + m[key] = append(m[key], fieldWithPrefix{ + fieldInfo: f, + prefix: searchPrefix, + }) + } + } + return m, errs +} + +type fieldWithPrefix struct { + *fieldInfo + prefix string +} + +// isEmptyFields returns true if all of specified fields are empty. +func isEmptyFields(fields []fieldWithPrefix, src map[string][]string) bool { + for _, f := range fields { + for _, path := range f.paths(f.prefix) { + v, ok := src[path] + if ok && !isEmpty(f.typ, v) { + return false + } + for key := range src { + if !isEmpty(f.typ, src[key]) && strings.HasPrefix(key, path) { + return false + } + } + } + } + return true +} + +// isEmpty returns true if value is empty for specific type +func isEmpty(t reflect.Type, value []string) bool { + if len(value) == 0 { + return true + } + switch t.Kind() { + case boolType, float32Type, float64Type, intType, int8Type, int32Type, int64Type, stringType, uint8Type, uint16Type, uint32Type, uint64Type: + return len(value[0]) == 0 + } + return false +} + +// decode fills a struct field using a parsed path. +func (d *Decoder) decode(v reflect.Value, path string, parts []pathPart, values []string) error { + // Get the field walking the struct fields by index. + for _, name := range parts[0].path { + if v.Type().Kind() == reflect.Ptr { + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + v = v.Elem() + } + + // alloc embedded structs + if v.Type().Kind() == reflect.Struct { + for i := 0; i < v.NumField(); i++ { + field := v.Field(i) + if field.Type().Kind() == reflect.Ptr && field.IsNil() && v.Type().Field(i).Anonymous == true { + field.Set(reflect.New(field.Type().Elem())) + } + } + } + + v = v.FieldByName(name) + } + // Don't even bother for unexported fields. + if !v.CanSet() { + return nil + } + + // Dereference if needed. + t := v.Type() + if t.Kind() == reflect.Ptr { + t = t.Elem() + if v.IsNil() { + v.Set(reflect.New(t)) + } + v = v.Elem() + } + + // Slice of structs. Let's go recursive. + if len(parts) > 1 { + idx := parts[0].index + if v.IsNil() || v.Len() < idx+1 { + value := reflect.MakeSlice(t, idx+1, idx+1) + if v.Len() < idx+1 { + // Resize it. + reflect.Copy(value, v) + } + v.Set(value) + } + return d.decode(v.Index(idx), path, parts[1:], values) + } + + // Get the converter early in case there is one for a slice type. + conv := d.cache.converter(t) + m := isTextUnmarshaler(v) + if conv == nil && t.Kind() == reflect.Slice && m.IsSliceElement { + var items []reflect.Value + elemT := t.Elem() + isPtrElem := elemT.Kind() == reflect.Ptr + if isPtrElem { + elemT = elemT.Elem() + } + + // Try to get a converter for the element type. + conv := d.cache.converter(elemT) + if conv == nil { + conv = builtinConverters[elemT.Kind()] + if conv == nil { + // As we are not dealing with slice of structs here, we don't need to check if the type + // implements TextUnmarshaler interface + return fmt.Errorf("schema: converter not found for %v", elemT) + } + } + + for key, value := range values { + if value == "" { + if d.zeroEmpty { + items = append(items, reflect.Zero(elemT)) + } + } else if m.IsValid { + u := reflect.New(elemT) + if m.IsSliceElementPtr { + u = reflect.New(reflect.PtrTo(elemT).Elem()) + } + if err := u.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value)); err != nil { + return ConversionError{ + Key: path, + Type: t, + Index: key, + Err: err, + } + } + if m.IsSliceElementPtr { + items = append(items, u.Elem().Addr()) + } else if u.Kind() == reflect.Ptr { + items = append(items, u.Elem()) + } else { + items = append(items, u) + } + } else if item := conv(value); item.IsValid() { + if isPtrElem { + ptr := reflect.New(elemT) + ptr.Elem().Set(item) + item = ptr + } + if item.Type() != elemT && !isPtrElem { + item = item.Convert(elemT) + } + items = append(items, item) + } else { + if strings.Contains(value, ",") { + values := strings.Split(value, ",") + for _, value := range values { + if value == "" { + if d.zeroEmpty { + items = append(items, reflect.Zero(elemT)) + } + } else if item := conv(value); item.IsValid() { + if isPtrElem { + ptr := reflect.New(elemT) + ptr.Elem().Set(item) + item = ptr + } + if item.Type() != elemT && !isPtrElem { + item = item.Convert(elemT) + } + items = append(items, item) + } else { + return ConversionError{ + Key: path, + Type: elemT, + Index: key, + } + } + } + } else { + return ConversionError{ + Key: path, + Type: elemT, + Index: key, + } + } + } + } + value := reflect.Append(reflect.MakeSlice(t, 0, 0), items...) + v.Set(value) + } else { + val := "" + // Use the last value provided if any values were provided + if len(values) > 0 { + val = values[len(values)-1] + } + + if conv != nil { + if value := conv(val); value.IsValid() { + v.Set(value.Convert(t)) + } else { + return ConversionError{ + Key: path, + Type: t, + Index: -1, + } + } + } else if m.IsValid { + if m.IsPtr { + u := reflect.New(v.Type()) + if err := u.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(val)); err != nil { + return ConversionError{ + Key: path, + Type: t, + Index: -1, + Err: err, + } + } + v.Set(reflect.Indirect(u)) + } else { + // If the value implements the encoding.TextUnmarshaler interface + // apply UnmarshalText as the converter + if err := m.Unmarshaler.UnmarshalText([]byte(val)); err != nil { + return ConversionError{ + Key: path, + Type: t, + Index: -1, + Err: err, + } + } + } + } else if val == "" { + if d.zeroEmpty { + v.Set(reflect.Zero(t)) + } + } else if conv := builtinConverters[t.Kind()]; conv != nil { + if value := conv(val); value.IsValid() { + v.Set(value.Convert(t)) + } else { + return ConversionError{ + Key: path, + Type: t, + Index: -1, + } + } + } else { + return fmt.Errorf("schema: converter not found for %v", t) + } + } + return nil +} + +func isTextUnmarshaler(v reflect.Value) unmarshaler { + // Create a new unmarshaller instance + m := unmarshaler{} + if m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler); m.IsValid { + return m + } + // As the UnmarshalText function should be applied to the pointer of the + // type, we check that type to see if it implements the necessary + // method. + if m.Unmarshaler, m.IsValid = reflect.New(v.Type()).Interface().(encoding.TextUnmarshaler); m.IsValid { + m.IsPtr = true + return m + } + + // if v is []T or *[]T create new T + t := v.Type() + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() == reflect.Slice { + // Check if the slice implements encoding.TextUnmarshaller + if m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler); m.IsValid { + return m + } + // If t is a pointer slice, check if its elements implement + // encoding.TextUnmarshaler + m.IsSliceElement = true + if t = t.Elem(); t.Kind() == reflect.Ptr { + t = reflect.PtrTo(t.Elem()) + v = reflect.Zero(t) + m.IsSliceElementPtr = true + m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler) + return m + } + } + + v = reflect.New(t) + m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler) + return m +} + +// TextUnmarshaler helpers ---------------------------------------------------- +// unmarshaller contains information about a TextUnmarshaler type +type unmarshaler struct { + Unmarshaler encoding.TextUnmarshaler + // IsValid indicates whether the resolved type indicated by the other + // flags implements the encoding.TextUnmarshaler interface. + IsValid bool + // IsPtr indicates that the resolved type is the pointer of the original + // type. + IsPtr bool + // IsSliceElement indicates that the resolved type is a slice element of + // the original type. + IsSliceElement bool + // IsSliceElementPtr indicates that the resolved type is a pointer to a + // slice element of the original type. + IsSliceElementPtr bool +} + +// Errors --------------------------------------------------------------------- + +// ConversionError stores information about a failed conversion. +type ConversionError struct { + Key string // key from the source map. + Type reflect.Type // expected type of elem + Index int // index for multi-value fields; -1 for single-value fields. + Err error // low-level error (when it exists) +} + +func (e ConversionError) Error() string { + var output string + + if e.Index < 0 { + output = fmt.Sprintf("schema: error converting value for %q", e.Key) + } else { + output = fmt.Sprintf("schema: error converting value for index %d of %q", + e.Index, e.Key) + } + + if e.Err != nil { + output = fmt.Sprintf("%s. Details: %s", output, e.Err) + } + + return output +} + +// UnknownKeyError stores information about an unknown key in the source map. +type UnknownKeyError struct { + Key string // key from the source map. +} + +func (e UnknownKeyError) Error() string { + return fmt.Sprintf("schema: invalid path %q", e.Key) +} + +// EmptyFieldError stores information about an empty required field. +type EmptyFieldError struct { + Key string // required key in the source map. +} + +func (e EmptyFieldError) Error() string { + return fmt.Sprintf("%v is empty", e.Key) +} + +// MultiError stores multiple decoding errors. +// +// Borrowed from the App Engine SDK. +type MultiError map[string]error + +func (e MultiError) Error() string { + s := "" + for _, err := range e { + s = err.Error() + break + } + switch len(e) { + case 0: + return "(0 errors)" + case 1: + return s + case 2: + return s + " (and 1 other error)" + } + return fmt.Sprintf("%s (and %d other errors)", s, len(e)-1) +} + +func (e MultiError) merge(errors MultiError) { + for key, err := range errors { + if e[key] == nil { + e[key] = err + } + } +} diff --git a/utils/schema/decoder_test.go b/utils/schema/decoder_test.go new file mode 100644 index 00000000..863891f6 --- /dev/null +++ b/utils/schema/decoder_test.go @@ -0,0 +1,2026 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package schema + +import ( + "encoding/hex" + "errors" + "reflect" + "strings" + "testing" + "time" +) + +type IntAlias int + +type rudeBool bool + +func (id *rudeBool) UnmarshalText(text []byte) error { + value := string(text) + switch { + case strings.EqualFold("Yup", value): + *id = true + case strings.EqualFold("Nope", value): + *id = false + default: + return errors.New("value must be yup or nope") + } + return nil +} + +// All cases we want to cover, in a nutshell. +type S1 struct { + F01 int `schema:"f1"` + F02 *int `schema:"f2"` + F03 []int `schema:"f3"` + F04 []*int `schema:"f4"` + F05 *[]int `schema:"f5"` + F06 *[]*int `schema:"f6"` + F07 S2 `schema:"f7"` + F08 *S1 `schema:"f8"` + F09 int `schema:"-"` + F10 []S1 `schema:"f10"` + F11 []*S1 `schema:"f11"` + F12 *[]S1 `schema:"f12"` + F13 *[]*S1 `schema:"f13"` + F14 int `schema:"f14"` + F15 IntAlias `schema:"f15"` + F16 []IntAlias `schema:"f16"` + F17 S19 `schema:"f17"` + F18 rudeBool `schema:"f18"` + F19 *rudeBool `schema:"f19"` + F20 []rudeBool `schema:"f20"` + F21 []*rudeBool `schema:"f21"` +} + +type S2 struct { + F01 *[]*int `schema:"f1"` +} + +type S19 [2]byte + +func (id *S19) UnmarshalText(text []byte) error { + buf, err := hex.DecodeString(string(text)) + if err != nil { + return err + } + if len(buf) > len(*id) { + return errors.New("out of range") + } + for i := range buf { + (*id)[i] = buf[i] + } + return nil +} + +func TestAll(t *testing.T) { + v := map[string][]string{ + "f1": {"1"}, + "f2": {"2"}, + "f3": {"31", "32"}, + "f4": {"41", "42"}, + "f5": {"51", "52"}, + "f6": {"61", "62"}, + "f7.f1": {"71", "72"}, + "f8.f8.f7.f1": {"81", "82"}, + "f9": {"9"}, + "f10.0.f10.0.f6": {"101", "102"}, + "f10.0.f10.1.f6": {"103", "104"}, + "f11.0.f11.0.f6": {"111", "112"}, + "f11.0.f11.1.f6": {"113", "114"}, + "f12.0.f12.0.f6": {"121", "122"}, + "f12.0.f12.1.f6": {"123", "124"}, + "f13.0.f13.0.f6": {"131", "132"}, + "f13.0.f13.1.f6": {"133", "134"}, + "f14": {}, + "f15": {"151"}, + "f16": {"161", "162"}, + "f17": {"1a2b"}, + "f18": {"yup"}, + "f19": {"nope"}, + "f20": {"nope", "yup"}, + "f21": {"yup", "nope"}, + } + f2 := 2 + f41, f42 := 41, 42 + f61, f62 := 61, 62 + f71, f72 := 71, 72 + f81, f82 := 81, 82 + f101, f102, f103, f104 := 101, 102, 103, 104 + f111, f112, f113, f114 := 111, 112, 113, 114 + f121, f122, f123, f124 := 121, 122, 123, 124 + f131, f132, f133, f134 := 131, 132, 133, 134 + var f151 IntAlias = 151 + var f161, f162 IntAlias = 161, 162 + var f152, f153 rudeBool = true, false + e := S1{ + F01: 1, + F02: &f2, + F03: []int{31, 32}, + F04: []*int{&f41, &f42}, + F05: &[]int{51, 52}, + F06: &[]*int{&f61, &f62}, + F07: S2{ + F01: &[]*int{&f71, &f72}, + }, + F08: &S1{ + F08: &S1{ + F07: S2{ + F01: &[]*int{&f81, &f82}, + }, + }, + }, + F09: 0, + F10: []S1{ + S1{ + F10: []S1{ + S1{F06: &[]*int{&f101, &f102}}, + S1{F06: &[]*int{&f103, &f104}}, + }, + }, + }, + F11: []*S1{ + &S1{ + F11: []*S1{ + &S1{F06: &[]*int{&f111, &f112}}, + &S1{F06: &[]*int{&f113, &f114}}, + }, + }, + }, + F12: &[]S1{ + S1{ + F12: &[]S1{ + S1{F06: &[]*int{&f121, &f122}}, + S1{F06: &[]*int{&f123, &f124}}, + }, + }, + }, + F13: &[]*S1{ + &S1{ + F13: &[]*S1{ + &S1{F06: &[]*int{&f131, &f132}}, + &S1{F06: &[]*int{&f133, &f134}}, + }, + }, + }, + F14: 0, + F15: f151, + F16: []IntAlias{f161, f162}, + F17: S19{0x1a, 0x2b}, + F18: f152, + F19: &f153, + F20: []rudeBool{f153, f152}, + F21: []*rudeBool{&f152, &f153}, + } + + s := &S1{} + _ = NewDecoder().Decode(s, v) + + vals := func(values []*int) []int { + r := make([]int, len(values)) + for k, v := range values { + r[k] = *v + } + return r + } + + if s.F01 != e.F01 { + t.Errorf("f1: expected %v, got %v", e.F01, s.F01) + } + if s.F02 == nil { + t.Errorf("f2: expected %v, got nil", *e.F02) + } else if *s.F02 != *e.F02 { + t.Errorf("f2: expected %v, got %v", *e.F02, *s.F02) + } + if s.F03 == nil { + t.Errorf("f3: expected %v, got nil", e.F03) + } else if len(s.F03) != 2 || s.F03[0] != e.F03[0] || s.F03[1] != e.F03[1] { + t.Errorf("f3: expected %v, got %v", e.F03, s.F03) + } + if s.F04 == nil { + t.Errorf("f4: expected %v, got nil", e.F04) + } else { + if len(s.F04) != 2 || *(s.F04)[0] != *(e.F04)[0] || *(s.F04)[1] != *(e.F04)[1] { + t.Errorf("f4: expected %v, got %v", vals(e.F04), vals(s.F04)) + } + } + if s.F05 == nil { + t.Errorf("f5: expected %v, got nil", e.F05) + } else { + sF05, eF05 := *s.F05, *e.F05 + if len(sF05) != 2 || sF05[0] != eF05[0] || sF05[1] != eF05[1] { + t.Errorf("f5: expected %v, got %v", eF05, sF05) + } + } + if s.F06 == nil { + t.Errorf("f6: expected %v, got nil", vals(*e.F06)) + } else { + sF06, eF06 := *s.F06, *e.F06 + if len(sF06) != 2 || *(sF06)[0] != *(eF06)[0] || *(sF06)[1] != *(eF06)[1] { + t.Errorf("f6: expected %v, got %v", vals(eF06), vals(sF06)) + } + } + if s.F07.F01 == nil { + t.Errorf("f7.f1: expected %v, got nil", vals(*e.F07.F01)) + } else { + sF07, eF07 := *s.F07.F01, *e.F07.F01 + if len(sF07) != 2 || *(sF07)[0] != *(eF07)[0] || *(sF07)[1] != *(eF07)[1] { + t.Errorf("f7.f1: expected %v, got %v", vals(eF07), vals(sF07)) + } + } + if s.F08 == nil { + t.Errorf("f8: got nil") + } else if s.F08.F08 == nil { + t.Errorf("f8.f8: got nil") + } else if s.F08.F08.F07.F01 == nil { + t.Errorf("f8.f8.f7.f1: expected %v, got nil", vals(*e.F08.F08.F07.F01)) + } else { + sF08, eF08 := *s.F08.F08.F07.F01, *e.F08.F08.F07.F01 + if len(sF08) != 2 || *(sF08)[0] != *(eF08)[0] || *(sF08)[1] != *(eF08)[1] { + t.Errorf("f8.f8.f7.f1: expected %v, got %v", vals(eF08), vals(sF08)) + } + } + if s.F09 != e.F09 { + t.Errorf("f9: expected %v, got %v", e.F09, s.F09) + } + if s.F10 == nil { + t.Errorf("f10: got nil") + } else if len(s.F10) != 1 { + t.Errorf("f10: expected 1 element, got %v", s.F10) + } else { + if len(s.F10[0].F10) != 2 { + t.Errorf("f10.0.f10: expected 1 element, got %v", s.F10[0].F10) + } else { + sF10, eF10 := *s.F10[0].F10[0].F06, *e.F10[0].F10[0].F06 + if sF10 == nil { + t.Errorf("f10.0.f10.0.f6: expected %v, got nil", vals(eF10)) + } else { + if len(sF10) != 2 || *(sF10)[0] != *(eF10)[0] || *(sF10)[1] != *(eF10)[1] { + t.Errorf("f10.0.f10.0.f6: expected %v, got %v", vals(eF10), vals(sF10)) + } + } + sF10, eF10 = *s.F10[0].F10[1].F06, *e.F10[0].F10[1].F06 + if sF10 == nil { + t.Errorf("f10.0.f10.0.f6: expected %v, got nil", vals(eF10)) + } else { + if len(sF10) != 2 || *(sF10)[0] != *(eF10)[0] || *(sF10)[1] != *(eF10)[1] { + t.Errorf("f10.0.f10.0.f6: expected %v, got %v", vals(eF10), vals(sF10)) + } + } + } + } + if s.F11 == nil { + t.Errorf("f11: got nil") + } else if len(s.F11) != 1 { + t.Errorf("f11: expected 1 element, got %v", s.F11) + } else { + if len(s.F11[0].F11) != 2 { + t.Errorf("f11.0.f11: expected 1 element, got %v", s.F11[0].F11) + } else { + sF11, eF11 := *s.F11[0].F11[0].F06, *e.F11[0].F11[0].F06 + if sF11 == nil { + t.Errorf("f11.0.f11.0.f6: expected %v, got nil", vals(eF11)) + } else { + if len(sF11) != 2 || *(sF11)[0] != *(eF11)[0] || *(sF11)[1] != *(eF11)[1] { + t.Errorf("f11.0.f11.0.f6: expected %v, got %v", vals(eF11), vals(sF11)) + } + } + sF11, eF11 = *s.F11[0].F11[1].F06, *e.F11[0].F11[1].F06 + if sF11 == nil { + t.Errorf("f11.0.f11.0.f6: expected %v, got nil", vals(eF11)) + } else { + if len(sF11) != 2 || *(sF11)[0] != *(eF11)[0] || *(sF11)[1] != *(eF11)[1] { + t.Errorf("f11.0.f11.0.f6: expected %v, got %v", vals(eF11), vals(sF11)) + } + } + } + } + if s.F12 == nil { + t.Errorf("f12: got nil") + } else if len(*s.F12) != 1 { + t.Errorf("f12: expected 1 element, got %v", *s.F12) + } else { + sF12, eF12 := *(s.F12), *(e.F12) + if len(*sF12[0].F12) != 2 { + t.Errorf("f12.0.f12: expected 1 element, got %v", *sF12[0].F12) + } else { + sF122, eF122 := *(*sF12[0].F12)[0].F06, *(*eF12[0].F12)[0].F06 + if sF122 == nil { + t.Errorf("f12.0.f12.0.f6: expected %v, got nil", vals(eF122)) + } else { + if len(sF122) != 2 || *(sF122)[0] != *(eF122)[0] || *(sF122)[1] != *(eF122)[1] { + t.Errorf("f12.0.f12.0.f6: expected %v, got %v", vals(eF122), vals(sF122)) + } + } + sF122, eF122 = *(*sF12[0].F12)[1].F06, *(*eF12[0].F12)[1].F06 + if sF122 == nil { + t.Errorf("f12.0.f12.0.f6: expected %v, got nil", vals(eF122)) + } else { + if len(sF122) != 2 || *(sF122)[0] != *(eF122)[0] || *(sF122)[1] != *(eF122)[1] { + t.Errorf("f12.0.f12.0.f6: expected %v, got %v", vals(eF122), vals(sF122)) + } + } + } + } + if s.F13 == nil { + t.Errorf("f13: got nil") + } else if len(*s.F13) != 1 { + t.Errorf("f13: expected 1 element, got %v", *s.F13) + } else { + sF13, eF13 := *(s.F13), *(e.F13) + if len(*sF13[0].F13) != 2 { + t.Errorf("f13.0.f13: expected 1 element, got %v", *sF13[0].F13) + } else { + sF132, eF132 := *(*sF13[0].F13)[0].F06, *(*eF13[0].F13)[0].F06 + if sF132 == nil { + t.Errorf("f13.0.f13.0.f6: expected %v, got nil", vals(eF132)) + } else { + if len(sF132) != 2 || *(sF132)[0] != *(eF132)[0] || *(sF132)[1] != *(eF132)[1] { + t.Errorf("f13.0.f13.0.f6: expected %v, got %v", vals(eF132), vals(sF132)) + } + } + sF132, eF132 = *(*sF13[0].F13)[1].F06, *(*eF13[0].F13)[1].F06 + if sF132 == nil { + t.Errorf("f13.0.f13.0.f6: expected %v, got nil", vals(eF132)) + } else { + if len(sF132) != 2 || *(sF132)[0] != *(eF132)[0] || *(sF132)[1] != *(eF132)[1] { + t.Errorf("f13.0.f13.0.f6: expected %v, got %v", vals(eF132), vals(sF132)) + } + } + } + } + if s.F14 != e.F14 { + t.Errorf("f14: expected %v, got %v", e.F14, s.F14) + } + if s.F15 != e.F15 { + t.Errorf("f15: expected %v, got %v", e.F15, s.F15) + } + if s.F16 == nil { + t.Errorf("f16: nil") + } else if len(s.F16) != len(e.F16) { + t.Errorf("f16: expected len %d, got %d", len(e.F16), len(s.F16)) + } else if !reflect.DeepEqual(s.F16, e.F16) { + t.Errorf("f16: expected %v, got %v", e.F16, s.F16) + } + if s.F17 != e.F17 { + t.Errorf("f17: expected %v, got %v", e.F17, s.F17) + } + if s.F18 != e.F18 { + t.Errorf("f18: expected %v, got %v", e.F18, s.F18) + } + if *s.F19 != *e.F19 { + t.Errorf("f19: expected %v, got %v", *e.F19, *s.F19) + } + if s.F20 == nil { + t.Errorf("f20: nil") + } else if len(s.F20) != len(e.F20) { + t.Errorf("f20: expected %v, got %v", e.F20, s.F20) + } else if !reflect.DeepEqual(s.F20, e.F20) { + t.Errorf("f20: expected %v, got %v", e.F20, s.F20) + } + if s.F21 == nil { + t.Errorf("f21: nil") + } else if len(s.F21) != len(e.F21) { + t.Errorf("f21: expected length %d, got %d", len(e.F21), len(s.F21)) + } else if !reflect.DeepEqual(s.F21, e.F21) { + t.Errorf("f21: expected %v, got %v", e.F21, s.F21) + } +} + +func BenchmarkAll(b *testing.B) { + v := map[string][]string{ + "f1": {"1"}, + "f2": {"2"}, + "f3": {"31", "32"}, + "f4": {"41", "42"}, + "f5": {"51", "52"}, + "f6": {"61", "62"}, + "f7.f1": {"71", "72"}, + "f8.f8.f7.f1": {"81", "82"}, + "f9": {"9"}, + "f10.0.f10.0.f6": {"101", "102"}, + "f10.0.f10.1.f6": {"103", "104"}, + "f11.0.f11.0.f6": {"111", "112"}, + "f11.0.f11.1.f6": {"113", "114"}, + "f12.0.f12.0.f6": {"121", "122"}, + "f12.0.f12.1.f6": {"123", "124"}, + "f13.0.f13.0.f6": {"131", "132"}, + "f13.0.f13.1.f6": {"133", "134"}, + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + s := &S1{} + _ = NewDecoder().Decode(s, v) + } +} + +// ---------------------------------------------------------------------------- + +type S3 struct { + F01 bool + F02 float32 + F03 float64 + F04 int + F05 int8 + F06 int16 + F07 int32 + F08 int64 + F09 string + F10 uint + F11 uint8 + F12 uint16 + F13 uint32 + F14 uint64 +} + +func TestDefaultConverters(t *testing.T) { + v := map[string][]string{ + "F01": {"true"}, + "F02": {"4.2"}, + "F03": {"4.3"}, + "F04": {"-42"}, + "F05": {"-43"}, + "F06": {"-44"}, + "F07": {"-45"}, + "F08": {"-46"}, + "F09": {"foo"}, + "F10": {"42"}, + "F11": {"43"}, + "F12": {"44"}, + "F13": {"45"}, + "F14": {"46"}, + } + e := S3{ + F01: true, + F02: 4.2, + F03: 4.3, + F04: -42, + F05: -43, + F06: -44, + F07: -45, + F08: -46, + F09: "foo", + F10: 42, + F11: 43, + F12: 44, + F13: 45, + F14: 46, + } + s := &S3{} + _ = NewDecoder().Decode(s, v) + if s.F01 != e.F01 { + t.Errorf("F01: expected %v, got %v", e.F01, s.F01) + } + if s.F02 != e.F02 { + t.Errorf("F02: expected %v, got %v", e.F02, s.F02) + } + if s.F03 != e.F03 { + t.Errorf("F03: expected %v, got %v", e.F03, s.F03) + } + if s.F04 != e.F04 { + t.Errorf("F04: expected %v, got %v", e.F04, s.F04) + } + if s.F05 != e.F05 { + t.Errorf("F05: expected %v, got %v", e.F05, s.F05) + } + if s.F06 != e.F06 { + t.Errorf("F06: expected %v, got %v", e.F06, s.F06) + } + if s.F07 != e.F07 { + t.Errorf("F07: expected %v, got %v", e.F07, s.F07) + } + if s.F08 != e.F08 { + t.Errorf("F08: expected %v, got %v", e.F08, s.F08) + } + if s.F09 != e.F09 { + t.Errorf("F09: expected %v, got %v", e.F09, s.F09) + } + if s.F10 != e.F10 { + t.Errorf("F10: expected %v, got %v", e.F10, s.F10) + } + if s.F11 != e.F11 { + t.Errorf("F11: expected %v, got %v", e.F11, s.F11) + } + if s.F12 != e.F12 { + t.Errorf("F12: expected %v, got %v", e.F12, s.F12) + } + if s.F13 != e.F13 { + t.Errorf("F13: expected %v, got %v", e.F13, s.F13) + } + if s.F14 != e.F14 { + t.Errorf("F14: expected %v, got %v", e.F14, s.F14) + } +} + +func TestOn(t *testing.T) { + v := map[string][]string{ + "F01": {"on"}, + } + s := S3{} + err := NewDecoder().Decode(&s, v) + if err != nil { + t.Fatal(err) + } + if !s.F01 { + t.Fatal("Value was not set to true") + } +} + +// ---------------------------------------------------------------------------- + +func TestInlineStruct(t *testing.T) { + s1 := &struct { + F01 bool + }{} + s2 := &struct { + F01 int + }{} + v1 := map[string][]string{ + "F01": {"true"}, + } + v2 := map[string][]string{ + "F01": {"42"}, + } + decoder := NewDecoder() + _ = decoder.Decode(s1, v1) + if s1.F01 != true { + t.Errorf("s1: expected %v, got %v", true, s1.F01) + } + _ = decoder.Decode(s2, v2) + if s2.F01 != 42 { + t.Errorf("s2: expected %v, got %v", 42, s2.F01) + } +} + +// ---------------------------------------------------------------------------- + +type Foo struct { + F01 int + F02 Bar + Bif []Baz +} + +type Bar struct { + F01 string + F02 string + F03 string + F14 string + S05 string + Str string +} + +type Baz struct { + F99 []string +} + +func TestSimpleExample(t *testing.T) { + data := map[string][]string{ + "F01": {"1"}, + "F02.F01": {"S1"}, + "F02.F02": {"S2"}, + "F02.F03": {"S3"}, + "F02.F14": {"S4"}, + "F02.S05": {"S5"}, + "F02.Str": {"Str"}, + "Bif.0.F99": {"A", "B", "C"}, + } + + e := &Foo{ + F01: 1, + F02: Bar{ + F01: "S1", + F02: "S2", + F03: "S3", + F14: "S4", + S05: "S5", + Str: "Str", + }, + Bif: []Baz{{ + F99: []string{"A", "B", "C"}}, + }, + } + + s := &Foo{} + _ = NewDecoder().Decode(s, data) + + if s.F01 != e.F01 { + t.Errorf("F01: expected %v, got %v", e.F01, s.F01) + } + if s.F02.F01 != e.F02.F01 { + t.Errorf("F02.F01: expected %v, got %v", e.F02.F01, s.F02.F01) + } + if s.F02.F02 != e.F02.F02 { + t.Errorf("F02.F02: expected %v, got %v", e.F02.F02, s.F02.F02) + } + if s.F02.F03 != e.F02.F03 { + t.Errorf("F02.F03: expected %v, got %v", e.F02.F03, s.F02.F03) + } + if s.F02.F14 != e.F02.F14 { + t.Errorf("F02.F14: expected %v, got %v", e.F02.F14, s.F02.F14) + } + if s.F02.S05 != e.F02.S05 { + t.Errorf("F02.S05: expected %v, got %v", e.F02.S05, s.F02.S05) + } + if s.F02.Str != e.F02.Str { + t.Errorf("F02.Str: expected %v, got %v", e.F02.Str, s.F02.Str) + } + if len(s.Bif) != len(e.Bif) { + t.Errorf("Bif len: expected %d, got %d", len(e.Bif), len(s.Bif)) + } else { + if len(s.Bif[0].F99) != len(e.Bif[0].F99) { + t.Errorf("Bif[0].F99 len: expected %d, got %d", len(e.Bif[0].F99), len(s.Bif[0].F99)) + } + } +} + +// ---------------------------------------------------------------------------- + +type S4 struct { + F01 int64 + F02 float64 + F03 bool + F04 rudeBool +} + +func TestConversionError(t *testing.T) { + data := map[string][]string{ + "F01": {"foo"}, + "F02": {"bar"}, + "F03": {"baz"}, + "F04": {"not-a-yes-or-nope"}, + } + s := &S4{} + e := NewDecoder().Decode(s, data) + + m := e.(MultiError) + if len(m) != 4 { + t.Errorf("Expected 3 errors, got %v", m) + } +} + +// ---------------------------------------------------------------------------- + +type S5 struct { + F01 []string +} + +func TestEmptyValue(t *testing.T) { + data := map[string][]string{ + "F01": {"", "foo"}, + } + s := &S5{} + NewDecoder().Decode(s, data) + if len(s.F01) != 1 { + t.Errorf("Expected 1 values in F01") + } +} + +func TestEmptyValueZeroEmpty(t *testing.T) { + data := map[string][]string{ + "F01": {"", "foo"}, + } + s := S5{} + d := NewDecoder() + d.ZeroEmpty(true) + err := d.Decode(&s, data) + if err != nil { + t.Fatal(err) + } + if len(s.F01) != 2 { + t.Errorf("Expected 1 values in F01") + } +} + +// ---------------------------------------------------------------------------- + +type S6 struct { + id string +} + +func TestUnexportedField(t *testing.T) { + data := map[string][]string{ + "id": {"identifier"}, + } + s := &S6{} + NewDecoder().Decode(s, data) + if s.id != "" { + t.Errorf("Unexported field expected to be ignored") + } +} + +// ---------------------------------------------------------------------------- + +type S7 struct { + ID string +} + +func TestMultipleValues(t *testing.T) { + data := map[string][]string{ + "ID": {"0", "1"}, + } + + s := S7{} + NewDecoder().Decode(&s, data) + if s.ID != "1" { + t.Errorf("Last defined value must be used when multiple values for same field are provided") + } +} + +type S8 struct { + ID string `json:"id"` +} + +func TestSetAliasTag(t *testing.T) { + data := map[string][]string{ + "id": {"foo"}, + } + + s := S8{} + dec := NewDecoder() + dec.SetAliasTag("json") + dec.Decode(&s, data) + if s.ID != "foo" { + t.Fatalf("Bad value: got %q, want %q", s.ID, "foo") + } +} + +func TestZeroEmpty(t *testing.T) { + data := map[string][]string{ + "F01": {""}, + "F03": {"true"}, + } + s := S4{1, 1, false, false} + d := NewDecoder() + d.ZeroEmpty(true) + + err := d.Decode(&s, data) + if err != nil { + t.Fatal(err) + } + if s.F01 != 0 { + t.Errorf("F01: got %v, want %v", s.F01, 0) + } + if s.F02 != 1 { + t.Errorf("F02: got %v, want %v", s.F02, 1) + } + if s.F03 != true { + t.Errorf("F03: got %v, want %v", s.F03, true) + } +} + +func TestNoZeroEmpty(t *testing.T) { + data := map[string][]string{ + "F01": {""}, + "F03": {"true"}, + } + s := S4{1, 1, false, false} + d := NewDecoder() + d.ZeroEmpty(false) + err := d.Decode(&s, data) + if err != nil { + t.Fatal(err) + } + if s.F01 != 1 { + t.Errorf("F01: got %v, want %v", s.F01, 1) + } + if s.F02 != 1 { + t.Errorf("F02: got %v, want %v", s.F02, 1) + } + if s.F03 != true { + t.Errorf("F03: got %v, want %v", s.F03, true) + } + if s.F04 != false { + t.Errorf("F04: got %v, want %v", s.F04, false) + } +} + +// ---------------------------------------------------------------------------- + +type S9 struct { + Id string +} + +type S10 struct { + S9 +} + +func TestEmbeddedField(t *testing.T) { + data := map[string][]string{ + "Id": {"identifier"}, + } + s := &S10{} + NewDecoder().Decode(s, data) + if s.Id != "identifier" { + t.Errorf("Missing support for embedded fields") + } +} + +type S11 struct { + S10 +} + +func TestMultipleLevelEmbeddedField(t *testing.T) { + data := map[string][]string{ + "Id": {"identifier"}, + } + s := &S11{} + err := NewDecoder().Decode(s, data) + if s.Id != "identifier" { + t.Errorf("Missing support for multiple-level embedded fields (%v)", err) + } +} + +func TestInvalidPath(t *testing.T) { + data := map[string][]string{ + "Foo.Bar": {"baz"}, + } + s := S9{} + err := NewDecoder().Decode(&s, data) + expectedErr := `schema: invalid path "Foo.Bar"` + if err.Error() != expectedErr { + t.Fatalf("got %q, want %q", err, expectedErr) + } +} + +func TestInvalidPathIgnoreUnknownKeys(t *testing.T) { + data := map[string][]string{ + "Foo.Bar": {"baz"}, + } + s := S9{} + dec := NewDecoder() + dec.IgnoreUnknownKeys(true) + err := dec.Decode(&s, data) + if err != nil { + t.Fatal(err) + } +} + +// ---------------------------------------------------------------------------- + +type S1NT struct { + F1 int + F2 *int + F3 []int + F4 []*int + F5 *[]int + F6 *[]*int + F7 S2 + F8 *S1 + F9 int `schema:"-"` + F10 []S1 + F11 []*S1 + F12 *[]S1 + F13 *[]*S1 +} + +func TestAllNT(t *testing.T) { + v := map[string][]string{ + "f1": {"1"}, + "f2": {"2"}, + "f3": {"31", "32"}, + "f4": {"41", "42"}, + "f5": {"51", "52"}, + "f6": {"61", "62"}, + "f7.f1": {"71", "72"}, + "f8.f8.f7.f1": {"81", "82"}, + "f9": {"9"}, + "f10.0.f10.0.f6": {"101", "102"}, + "f10.0.f10.1.f6": {"103", "104"}, + "f11.0.f11.0.f6": {"111", "112"}, + "f11.0.f11.1.f6": {"113", "114"}, + "f12.0.f12.0.f6": {"121", "122"}, + "f12.0.f12.1.f6": {"123", "124"}, + "f13.0.f13.0.f6": {"131", "132"}, + "f13.0.f13.1.f6": {"133", "134"}, + } + f2 := 2 + f41, f42 := 41, 42 + f61, f62 := 61, 62 + f71, f72 := 71, 72 + f81, f82 := 81, 82 + f101, f102, f103, f104 := 101, 102, 103, 104 + f111, f112, f113, f114 := 111, 112, 113, 114 + f121, f122, f123, f124 := 121, 122, 123, 124 + f131, f132, f133, f134 := 131, 132, 133, 134 + e := S1NT{ + F1: 1, + F2: &f2, + F3: []int{31, 32}, + F4: []*int{&f41, &f42}, + F5: &[]int{51, 52}, + F6: &[]*int{&f61, &f62}, + F7: S2{ + F01: &[]*int{&f71, &f72}, + }, + F8: &S1{ + F08: &S1{ + F07: S2{ + F01: &[]*int{&f81, &f82}, + }, + }, + }, + F9: 0, + F10: []S1{ + S1{ + F10: []S1{ + S1{F06: &[]*int{&f101, &f102}}, + S1{F06: &[]*int{&f103, &f104}}, + }, + }, + }, + F11: []*S1{ + &S1{ + F11: []*S1{ + &S1{F06: &[]*int{&f111, &f112}}, + &S1{F06: &[]*int{&f113, &f114}}, + }, + }, + }, + F12: &[]S1{ + S1{ + F12: &[]S1{ + S1{F06: &[]*int{&f121, &f122}}, + S1{F06: &[]*int{&f123, &f124}}, + }, + }, + }, + F13: &[]*S1{ + &S1{ + F13: &[]*S1{ + &S1{F06: &[]*int{&f131, &f132}}, + &S1{F06: &[]*int{&f133, &f134}}, + }, + }, + }, + } + + s := &S1NT{} + _ = NewDecoder().Decode(s, v) + + vals := func(values []*int) []int { + r := make([]int, len(values)) + for k, v := range values { + r[k] = *v + } + return r + } + + if s.F1 != e.F1 { + t.Errorf("f1: expected %v, got %v", e.F1, s.F1) + } + if s.F2 == nil { + t.Errorf("f2: expected %v, got nil", *e.F2) + } else if *s.F2 != *e.F2 { + t.Errorf("f2: expected %v, got %v", *e.F2, *s.F2) + } + if s.F3 == nil { + t.Errorf("f3: expected %v, got nil", e.F3) + } else if len(s.F3) != 2 || s.F3[0] != e.F3[0] || s.F3[1] != e.F3[1] { + t.Errorf("f3: expected %v, got %v", e.F3, s.F3) + } + if s.F4 == nil { + t.Errorf("f4: expected %v, got nil", e.F4) + } else { + if len(s.F4) != 2 || *(s.F4)[0] != *(e.F4)[0] || *(s.F4)[1] != *(e.F4)[1] { + t.Errorf("f4: expected %v, got %v", vals(e.F4), vals(s.F4)) + } + } + if s.F5 == nil { + t.Errorf("f5: expected %v, got nil", e.F5) + } else { + sF5, eF5 := *s.F5, *e.F5 + if len(sF5) != 2 || sF5[0] != eF5[0] || sF5[1] != eF5[1] { + t.Errorf("f5: expected %v, got %v", eF5, sF5) + } + } + if s.F6 == nil { + t.Errorf("f6: expected %v, got nil", vals(*e.F6)) + } else { + sF6, eF6 := *s.F6, *e.F6 + if len(sF6) != 2 || *(sF6)[0] != *(eF6)[0] || *(sF6)[1] != *(eF6)[1] { + t.Errorf("f6: expected %v, got %v", vals(eF6), vals(sF6)) + } + } + if s.F7.F01 == nil { + t.Errorf("f7.f1: expected %v, got nil", vals(*e.F7.F01)) + } else { + sF7, eF7 := *s.F7.F01, *e.F7.F01 + if len(sF7) != 2 || *(sF7)[0] != *(eF7)[0] || *(sF7)[1] != *(eF7)[1] { + t.Errorf("f7.f1: expected %v, got %v", vals(eF7), vals(sF7)) + } + } + if s.F8 == nil { + t.Errorf("f8: got nil") + } else if s.F8.F08 == nil { + t.Errorf("f8.f8: got nil") + } else if s.F8.F08.F07.F01 == nil { + t.Errorf("f8.f8.f7.f1: expected %v, got nil", vals(*e.F8.F08.F07.F01)) + } else { + sF8, eF8 := *s.F8.F08.F07.F01, *e.F8.F08.F07.F01 + if len(sF8) != 2 || *(sF8)[0] != *(eF8)[0] || *(sF8)[1] != *(eF8)[1] { + t.Errorf("f8.f8.f7.f1: expected %v, got %v", vals(eF8), vals(sF8)) + } + } + if s.F9 != e.F9 { + t.Errorf("f9: expected %v, got %v", e.F9, s.F9) + } + if s.F10 == nil { + t.Errorf("f10: got nil") + } else if len(s.F10) != 1 { + t.Errorf("f10: expected 1 element, got %v", s.F10) + } else { + if len(s.F10[0].F10) != 2 { + t.Errorf("f10.0.f10: expected 1 element, got %v", s.F10[0].F10) + } else { + sF10, eF10 := *s.F10[0].F10[0].F06, *e.F10[0].F10[0].F06 + if sF10 == nil { + t.Errorf("f10.0.f10.0.f6: expected %v, got nil", vals(eF10)) + } else { + if len(sF10) != 2 || *(sF10)[0] != *(eF10)[0] || *(sF10)[1] != *(eF10)[1] { + t.Errorf("f10.0.f10.0.f6: expected %v, got %v", vals(eF10), vals(sF10)) + } + } + sF10, eF10 = *s.F10[0].F10[1].F06, *e.F10[0].F10[1].F06 + if sF10 == nil { + t.Errorf("f10.0.f10.0.f6: expected %v, got nil", vals(eF10)) + } else { + if len(sF10) != 2 || *(sF10)[0] != *(eF10)[0] || *(sF10)[1] != *(eF10)[1] { + t.Errorf("f10.0.f10.0.f6: expected %v, got %v", vals(eF10), vals(sF10)) + } + } + } + } + if s.F11 == nil { + t.Errorf("f11: got nil") + } else if len(s.F11) != 1 { + t.Errorf("f11: expected 1 element, got %v", s.F11) + } else { + if len(s.F11[0].F11) != 2 { + t.Errorf("f11.0.f11: expected 1 element, got %v", s.F11[0].F11) + } else { + sF11, eF11 := *s.F11[0].F11[0].F06, *e.F11[0].F11[0].F06 + if sF11 == nil { + t.Errorf("f11.0.f11.0.f6: expected %v, got nil", vals(eF11)) + } else { + if len(sF11) != 2 || *(sF11)[0] != *(eF11)[0] || *(sF11)[1] != *(eF11)[1] { + t.Errorf("f11.0.f11.0.f6: expected %v, got %v", vals(eF11), vals(sF11)) + } + } + sF11, eF11 = *s.F11[0].F11[1].F06, *e.F11[0].F11[1].F06 + if sF11 == nil { + t.Errorf("f11.0.f11.0.f6: expected %v, got nil", vals(eF11)) + } else { + if len(sF11) != 2 || *(sF11)[0] != *(eF11)[0] || *(sF11)[1] != *(eF11)[1] { + t.Errorf("f11.0.f11.0.f6: expected %v, got %v", vals(eF11), vals(sF11)) + } + } + } + } + if s.F12 == nil { + t.Errorf("f12: got nil") + } else if len(*s.F12) != 1 { + t.Errorf("f12: expected 1 element, got %v", *s.F12) + } else { + sF12, eF12 := *(s.F12), *(e.F12) + if len(*sF12[0].F12) != 2 { + t.Errorf("f12.0.f12: expected 1 element, got %v", *sF12[0].F12) + } else { + sF122, eF122 := *(*sF12[0].F12)[0].F06, *(*eF12[0].F12)[0].F06 + if sF122 == nil { + t.Errorf("f12.0.f12.0.f6: expected %v, got nil", vals(eF122)) + } else { + if len(sF122) != 2 || *(sF122)[0] != *(eF122)[0] || *(sF122)[1] != *(eF122)[1] { + t.Errorf("f12.0.f12.0.f6: expected %v, got %v", vals(eF122), vals(sF122)) + } + } + sF122, eF122 = *(*sF12[0].F12)[1].F06, *(*eF12[0].F12)[1].F06 + if sF122 == nil { + t.Errorf("f12.0.f12.0.f6: expected %v, got nil", vals(eF122)) + } else { + if len(sF122) != 2 || *(sF122)[0] != *(eF122)[0] || *(sF122)[1] != *(eF122)[1] { + t.Errorf("f12.0.f12.0.f6: expected %v, got %v", vals(eF122), vals(sF122)) + } + } + } + } + if s.F13 == nil { + t.Errorf("f13: got nil") + } else if len(*s.F13) != 1 { + t.Errorf("f13: expected 1 element, got %v", *s.F13) + } else { + sF13, eF13 := *(s.F13), *(e.F13) + if len(*sF13[0].F13) != 2 { + t.Errorf("f13.0.f13: expected 1 element, got %v", *sF13[0].F13) + } else { + sF132, eF132 := *(*sF13[0].F13)[0].F06, *(*eF13[0].F13)[0].F06 + if sF132 == nil { + t.Errorf("f13.0.f13.0.f6: expected %v, got nil", vals(eF132)) + } else { + if len(sF132) != 2 || *(sF132)[0] != *(eF132)[0] || *(sF132)[1] != *(eF132)[1] { + t.Errorf("f13.0.f13.0.f6: expected %v, got %v", vals(eF132), vals(sF132)) + } + } + sF132, eF132 = *(*sF13[0].F13)[1].F06, *(*eF13[0].F13)[1].F06 + if sF132 == nil { + t.Errorf("f13.0.f13.0.f6: expected %v, got nil", vals(eF132)) + } else { + if len(sF132) != 2 || *(sF132)[0] != *(eF132)[0] || *(sF132)[1] != *(eF132)[1] { + t.Errorf("f13.0.f13.0.f6: expected %v, got %v", vals(eF132), vals(sF132)) + } + } + } + } +} + +// ---------------------------------------------------------------------------- + +type S12A struct { + ID []int +} + +func TestCSVSlice(t *testing.T) { + data := map[string][]string{ + "ID": {"0,1"}, + } + + s := S12A{} + NewDecoder().Decode(&s, data) + if len(s.ID) != 2 { + t.Errorf("Expected two values in the result list, got %+v", s.ID) + } + if s.ID[0] != 0 || s.ID[1] != 1 { + t.Errorf("Expected []{0, 1} got %+v", s) + } +} + +type S12B struct { + ID []string +} + +//Decode should not split on , into a slice for string only +func TestCSVStringSlice(t *testing.T) { + data := map[string][]string{ + "ID": {"0,1"}, + } + + s := S12B{} + NewDecoder().Decode(&s, data) + if len(s.ID) != 1 { + t.Errorf("Expected one value in the result list, got %+v", s.ID) + } + if s.ID[0] != "0,1" { + t.Errorf("Expected []{0, 1} got %+v", s) + } +} + +//Invalid data provided by client should not panic (github issue 33) +func TestInvalidDataProvidedByClient(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Errorf("Panicked calling decoder.Decode: %v", r) + } + }() + + type S struct { + f string + } + + data := map[string][]string{ + "f.f": {"v"}, + } + + err := NewDecoder().Decode(new(S), data) + if err == nil { + t.Errorf("invalid path in decoder.Decode should return an error.") + } +} + +// underlying cause of error in issue 33 +func TestInvalidPathInCacheParsePath(t *testing.T) { + type S struct { + f string + } + + typ := reflect.ValueOf(new(S)).Elem().Type() + c := newCache() + _, err := c.parsePath("f.f", typ) + if err == nil { + t.Errorf("invalid path in cache.parsePath should return an error.") + } +} + +// issue 32 +func TestDecodeToTypedField(t *testing.T) { + type Aa bool + s1 := &struct{ Aa }{} + v1 := map[string][]string{"Aa": {"true"}} + NewDecoder().Decode(s1, v1) + if s1.Aa != Aa(true) { + t.Errorf("s1: expected %v, got %v", true, s1.Aa) + } +} + +// issue 37 +func TestRegisterConverter(t *testing.T) { + type Aa int + type Bb int + s1 := &struct { + Aa + Bb + }{} + decoder := NewDecoder() + + decoder.RegisterConverter(s1.Aa, func(s string) reflect.Value { return reflect.ValueOf(1) }) + decoder.RegisterConverter(s1.Bb, func(s string) reflect.Value { return reflect.ValueOf(2) }) + + v1 := map[string][]string{"Aa": {"4"}, "Bb": {"5"}} + decoder.Decode(s1, v1) + + if s1.Aa != Aa(1) { + t.Errorf("s1.Aa: expected %v, got %v", 1, s1.Aa) + } + if s1.Bb != Bb(2) { + t.Errorf("s1.Bb: expected %v, got %v", 2, s1.Bb) + } +} + +// Issue #40 +func TestRegisterConverterSlice(t *testing.T) { + decoder := NewDecoder() + decoder.RegisterConverter([]string{}, func(input string) reflect.Value { + return reflect.ValueOf(strings.Split(input, ",")) + }) + + result := struct { + Multiple []string `schema:"multiple"` + }{} + + expected := []string{"one", "two", "three"} + decoder.Decode(&result, map[string][]string{ + "multiple": []string{"one,two,three"}, + }) + for i := range expected { + if got, want := expected[i], result.Multiple[i]; got != want { + t.Errorf("%d: got %s, want %s", i, got, want) + } + } +} + +func TestRegisterConverterMap(t *testing.T) { + decoder := NewDecoder() + decoder.IgnoreUnknownKeys(false) + decoder.RegisterConverter(map[string]string{}, func(input string) reflect.Value { + m := make(map[string]string) + for _, pair := range strings.Split(input, ",") { + parts := strings.Split(pair, ":") + switch len(parts) { + case 2: + m[parts[0]] = parts[1] + } + } + return reflect.ValueOf(m) + }) + + result := struct { + Multiple map[string]string `schema:"multiple"` + }{} + + err := decoder.Decode(&result, map[string][]string{ + "multiple": []string{"a:one,b:two"}, + }) + if err != nil { + t.Fatal(err) + } + expected := map[string]string{"a": "one", "b": "two"} + for k, v := range expected { + got, ok := result.Multiple[k] + if !ok { + t.Fatalf("got %v, want %v", result.Multiple, expected) + } + if got != v { + t.Errorf("got %s, want %s", got, v) + } + } +} + +type S13 struct { + Value []S14 +} + +type S14 struct { + F1 string + F2 string +} + +func (n *S14) UnmarshalText(text []byte) error { + textParts := strings.Split(string(text), " ") + if len(textParts) < 2 { + return errors.New("Not a valid name!") + } + + n.F1, n.F2 = textParts[0], textParts[len(textParts)-1] + return nil +} + +type S15 struct { + Value []S16 +} + +type S16 struct { + F1 string + F2 string +} + +func TestCustomTypeSlice(t *testing.T) { + data := map[string][]string{ + "Value.0": []string{"Louisa May Alcott"}, + "Value.1": []string{"Florence Nightingale"}, + "Value.2": []string{"Clara Barton"}, + } + + s := S13{} + decoder := NewDecoder() + + if err := decoder.Decode(&s, data); err != nil { + t.Fatal(err) + } + + if len(s.Value) != 3 { + t.Fatalf("Expected 3 values in the result list, got %+v", s.Value) + } + if s.Value[0].F1 != "Louisa" || s.Value[0].F2 != "Alcott" { + t.Errorf("Expected S14{'Louisa', 'Alcott'} got %+v", s.Value[0]) + } + if s.Value[1].F1 != "Florence" || s.Value[1].F2 != "Nightingale" { + t.Errorf("Expected S14{'Florence', 'Nightingale'} got %+v", s.Value[1]) + } + if s.Value[2].F1 != "Clara" || s.Value[2].F2 != "Barton" { + t.Errorf("Expected S14{'Clara', 'Barton'} got %+v", s.Value[2]) + } +} + +func TestCustomTypeSliceWithError(t *testing.T) { + data := map[string][]string{ + "Value.0": []string{"Louisa May Alcott"}, + "Value.1": []string{"Florence Nightingale"}, + "Value.2": []string{"Clara"}, + } + + s := S13{} + decoder := NewDecoder() + + if err := decoder.Decode(&s, data); err == nil { + t.Error("Not detecting error in conversion") + } +} + +func TestNoTextUnmarshalerTypeSlice(t *testing.T) { + data := map[string][]string{ + "Value.0": []string{"Louisa May Alcott"}, + "Value.1": []string{"Florence Nightingale"}, + "Value.2": []string{"Clara Barton"}, + } + + s := S15{} + decoder := NewDecoder() + + if err := decoder.Decode(&s, data); err == nil { + t.Error("Not detecting when there's no converter") + } +} + +// ---------------------------------------------------------------------------- + +type S17 struct { + Value S14 +} + +type S18 struct { + Value S16 +} + +func TestCustomType(t *testing.T) { + data := map[string][]string{ + "Value": []string{"Louisa May Alcott"}, + } + + s := S17{} + decoder := NewDecoder() + + if err := decoder.Decode(&s, data); err != nil { + t.Fatal(err) + } + + if s.Value.F1 != "Louisa" || s.Value.F2 != "Alcott" { + t.Errorf("Expected S14{'Louisa', 'Alcott'} got %+v", s.Value) + } +} + +func TestCustomTypeWithError(t *testing.T) { + data := map[string][]string{ + "Value": []string{"Louisa"}, + } + + s := S17{} + decoder := NewDecoder() + + if err := decoder.Decode(&s, data); err == nil { + t.Error("Not detecting error in conversion") + } +} + +func TestNoTextUnmarshalerType(t *testing.T) { + data := map[string][]string{ + "Value": []string{"Louisa May Alcott"}, + } + + s := S18{} + decoder := NewDecoder() + + if err := decoder.Decode(&s, data); err == nil { + t.Error("Not detecting when there's no converter") + } +} + +func TestExpectedType(t *testing.T) { + data := map[string][]string{ + "bools": []string{"1", "a"}, + "date": []string{"invalid"}, + "Foo.Bar": []string{"a", "b"}, + } + + type B struct { + Bar *int + } + type A struct { + Bools []bool `schema:"bools"` + Date time.Time `schema:"date"` + Foo B + } + + a := A{} + + err := NewDecoder().Decode(&a, data) + + e := err.(MultiError)["bools"].(ConversionError) + if e.Type != reflect.TypeOf(false) && e.Index == 1 { + t.Errorf("Expected bool, index: 1 got %+v, index: %d", e.Type, e.Index) + } + e = err.(MultiError)["date"].(ConversionError) + if e.Type != reflect.TypeOf(time.Time{}) { + t.Errorf("Expected time.Time got %+v", e.Type) + } + e = err.(MultiError)["Foo.Bar"].(ConversionError) + if e.Type != reflect.TypeOf(0) { + t.Errorf("Expected int got %+v", e.Type) + } +} + +type R1 struct { + A string `schema:"a,required"` + B struct { + C int `schema:"c,required"` + D float64 `schema:"d"` + E string `schema:"e,required"` + } `schema:"b"` + F []string `schema:"f,required"` + G []int `schema:"g,othertag"` + H bool `schema:"h,required"` +} + +func TestRequiredField(t *testing.T) { + var a R1 + v := map[string][]string{ + "a": []string{"bbb"}, + "b.c": []string{"88"}, + "b.d": []string{"9"}, + "f": []string{""}, + "h": []string{"true"}, + } + err := NewDecoder().Decode(&a, v) + if err == nil { + t.Errorf("error nil, b.e is empty expect") + return + } + // b.e empty + v["b.e"] = []string{""} // empty string + err = NewDecoder().Decode(&a, v) + if err == nil { + t.Errorf("error nil, b.e is empty expect") + return + } + if expected := `b.e is empty`; err.Error() != expected { + t.Errorf("got %q, want %q", err, expected) + } + + // all fields ok + v["b.e"] = []string{"nonempty"} + err = NewDecoder().Decode(&a, v) + if err != nil { + t.Errorf("error: %v", err) + return + } + + // set f empty + v["f"] = []string{} + err = NewDecoder().Decode(&a, v) + if err == nil { + t.Errorf("error nil, f is empty expect") + return + } + if expected := `f is empty`; err.Error() != expected { + t.Errorf("got %q, want %q", err, expected) + } + v["f"] = []string{"nonempty"} + + // b.c type int with empty string + v["b.c"] = []string{""} + err = NewDecoder().Decode(&a, v) + if err == nil { + t.Errorf("error nil, b.c is empty expect") + return + } + v["b.c"] = []string{"3"} + + // h type bool with empty string + v["h"] = []string{""} + err = NewDecoder().Decode(&a, v) + if err == nil { + t.Errorf("error nil, h is empty expect") + return + } + if expected := `h is empty`; err.Error() != expected { + t.Errorf("got %q, want %q", err, expected) + } +} + +type R2 struct { + A struct { + B int `schema:"b"` + } `schema:"a,required"` +} + +func TestRequiredStructFiled(t *testing.T) { + v := map[string][]string{ + "a.b": []string{"3"}, + } + var a R2 + err := NewDecoder().Decode(&a, v) + if err != nil { + t.Errorf("error: %v", err) + } +} + +func TestRequiredFieldIsMissingCorrectError(t *testing.T) { + type RM1S struct { + A string `schema:"rm1aa,required"` + B string `schema:"rm1bb,required"` + } + type RM1 struct { + RM1S + } + + var a RM1 + v := map[string][]string{ + "rm1aa": {"aaa"}, + } + expectedError := "RM1S.rm1bb is empty" + err := NewDecoder().Decode(&a, v) + if err.Error() != expectedError { + t.Errorf("expected %v, got %v", expectedError, err) + } +} + +type AS1 struct { + A int32 `schema:"a,required"` + E int32 `schema:"e,required"` +} +type AS2 struct { + AS1 + B string `schema:"b,required"` +} +type AS3 struct { + C int32 `schema:"c"` +} + +type AS4 struct { + AS3 + D string `schema:"d"` +} + +func TestAnonymousStructField(t *testing.T) { + patterns := []map[string][]string{ + { + "a": {"1"}, + "e": {"2"}, + "b": {"abc"}, + }, + { + "AS1.a": {"1"}, + "AS1.e": {"2"}, + "b": {"abc"}, + }, + } + for _, v := range patterns { + a := AS2{} + err := NewDecoder().Decode(&a, v) + if err != nil { + t.Errorf("Decode failed %s, %#v", err, v) + continue + } + if a.A != 1 { + t.Errorf("A: expected %v, got %v", 1, a.A) + } + if a.E != 2 { + t.Errorf("E: expected %v, got %v", 2, a.E) + } + if a.B != "abc" { + t.Errorf("B: expected %v, got %v", "abc", a.B) + } + if a.AS1.A != 1 { + t.Errorf("AS1.A: expected %v, got %v", 1, a.AS1.A) + } + if a.AS1.E != 2 { + t.Errorf("AS1.E: expected %v, got %v", 2, a.AS1.E) + } + } + a := AS2{} + err := NewDecoder().Decode(&a, map[string][]string{ + "e": {"2"}, + "b": {"abc"}, + }) + if err == nil { + t.Errorf("error nil, a is empty expect") + } + patterns = []map[string][]string{ + { + "c": {"1"}, + "d": {"abc"}, + }, + { + "AS3.c": {"1"}, + "d": {"abc"}, + }, + } + for _, v := range patterns { + a := AS4{} + err := NewDecoder().Decode(&a, v) + if err != nil { + t.Errorf("Decode failed %s, %#v", err, v) + continue + } + if a.C != 1 { + t.Errorf("C: expected %v, got %v", 1, a.C) + } + if a.D != "abc" { + t.Errorf("D: expected %v, got %v", "abc", a.D) + } + if a.AS3.C != 1 { + t.Errorf("AS3.C: expected %v, got %v", 1, a.AS3.C) + } + } +} + +func TestAmbiguousStructField(t *testing.T) { + type I1 struct { + X int + } + type I2 struct { + I1 + } + type B1 struct { + X bool + } + type B2 struct { + B1 + } + type IB struct { + I1 + B1 + } + type S struct { + I1 + I2 + B1 + B2 + IB + } + dst := S{} + src := map[string][]string{ + "X": {"123"}, + "IB.X": {"123"}, + } + dec := NewDecoder() + dec.IgnoreUnknownKeys(false) + err := dec.Decode(&dst, src) + e, ok := err.(MultiError) + if !ok || len(e) != 2 { + t.Errorf("Expected 2 errors, got %#v", err) + } + if expected := (UnknownKeyError{Key: "X"}); e["X"] != expected { + t.Errorf("X: expected %#v, got %#v", expected, e["X"]) + } + if expected := (UnknownKeyError{Key: "IB.X"}); e["IB.X"] != expected { + t.Errorf("X: expected %#v, got %#v", expected, e["IB.X"]) + } + dec.IgnoreUnknownKeys(true) + err = dec.Decode(&dst, src) + if err != nil { + t.Errorf("Decode failed %v", err) + } + + expected := S{ + I1: I1{X: 123}, + I2: I2{I1: I1{X: 234}}, + B1: B1{X: true}, + B2: B2{B1: B1{X: true}}, + IB: IB{I1: I1{X: 345}, B1: B1{X: true}}, + } + patterns := []map[string][]string{ + { + "I1.X": {"123"}, + "I2.X": {"234"}, + "B1.X": {"true"}, + "B2.X": {"1"}, + "IB.I1.X": {"345"}, + "IB.B1.X": {"on"}, + }, + { + "I1.X": {"123"}, + "I2.I1.X": {"234"}, + "B1.X": {"true"}, + "B2.B1.X": {"1"}, + "IB.I1.X": {"345"}, + "IB.B1.X": {"on"}, + }, + } + for _, src := range patterns { + dst := S{} + dec := NewDecoder() + dec.IgnoreUnknownKeys(false) + err := dec.Decode(&dst, src) + if err != nil { + t.Errorf("Decode failed %v, %#v", err, src) + } + if !reflect.DeepEqual(expected, dst) { + t.Errorf("Expected %+v, got %+v", expected, dst) + } + } +} + +func TestComprehensiveDecodingErrors(t *testing.T) { + type I1 struct { + V int `schema:",required"` + P *int `schema:",required"` + } + type I2 struct { + I1 + J I1 + } + type S1 struct { + V string `schema:"v,required"` + P *string `schema:"p,required"` + } + type S2 struct { + S1 `schema:"s"` + T S1 `schema:"t"` + } + type D struct { + I2 + X S2 `schema:"x"` + Y S2 `schema:"-"` + } + patterns := []map[string][]string{ + { + "V": {"invalid"}, // invalid + "I2.I1.P": {}, // empty + "I2.J.V": {""}, // empty + "I2.J.P": {"123"}, // ok + "x.s.v": {""}, // empty + "x.s.p": {""}, // ok + "x.t.v": {"abc"}, // ok + "x.t.p": {}, // empty + "Y.s.v": {"ignored"}, // unknown + }, + { + "V": {"invalid"}, // invalid + "P": {}, // empty + "J.V": {""}, // empty + "J.P": {"123"}, // ok + "x.v": {""}, // empty + "x.p": {""}, // ok + "x.t.v": {"abc"}, // ok + "x.t.p": {}, // empty + "Y.s.v": {"ignored"}, // unknown + }, + } + for _, src := range patterns { + dst := D{} + dec := NewDecoder() + dec.IgnoreUnknownKeys(false) + err := dec.Decode(&dst, src) + e, ok := err.(MultiError) + if !ok || len(e) != 6 { + t.Errorf("Expected 6 errors, got %#v", err) + } + if cerr, ok := e["V"].(ConversionError); !ok { + t.Errorf("%s: expected %#v, got %#v", "I2.I1.V", ConversionError{Key: "V"}, cerr) + } + if key, expected := "I2.I1.P", (EmptyFieldError{Key: "I2.I1.P"}); e[key] != expected { + t.Errorf("%s: expected %#v, got %#v", key, expected, e[key]) + } + if key, expected := "I2.J.V", (EmptyFieldError{Key: "I2.J.V"}); e[key] != expected { + t.Errorf("%s: expected %#v, got %#v", key, expected, e[key]) + } + if key, expected := "x.s.v", (EmptyFieldError{Key: "x.s.v"}); e[key] != expected { + t.Errorf("%s: expected %#v, got %#v", key, expected, e[key]) + } + if key, expected := "x.t.p", (EmptyFieldError{Key: "x.t.p"}); e[key] != expected { + t.Errorf("%s: expected %#v, got %#v", key, expected, e[key]) + } + if key, expected := "Y.s.v", (UnknownKeyError{Key: "Y.s.v"}); e[key] != expected { + t.Errorf("%s: expected %#v, got %#v", key, expected, e[key]) + } + if expected := 123; dst.I2.J.P == nil || *dst.I2.J.P != expected { + t.Errorf("I2.J.P: expected %#v, got %#v", expected, dst.I2.J.P) + } + if expected := ""; dst.X.S1.P == nil || *dst.X.S1.P != expected { + t.Errorf("X.S1.P: expected %#v, got %#v", expected, dst.X.S1.P) + } + if expected := "abc"; dst.X.T.V != expected { + t.Errorf("X.T.V: expected %#v, got %#v", expected, dst.X.T.V) + } + } +} + +// Test to ensure that a registered converter overrides the default text unmarshaler. +func TestRegisterConverterOverridesTextUnmarshaler(t *testing.T) { + type MyTime time.Time + s1 := &struct { + MyTime + }{} + decoder := NewDecoder() + + ts := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) + decoder.RegisterConverter(s1.MyTime, func(s string) reflect.Value { return reflect.ValueOf(ts) }) + + v1 := map[string][]string{"MyTime": {"4"}, "Bb": {"5"}} + decoder.Decode(s1, v1) + + if s1.MyTime != MyTime(ts) { + t.Errorf("s1.Aa: expected %v, got %v", ts, s1.MyTime) + } +} + +type S20E string + +func (e *S20E) UnmarshalText(text []byte) error { + *e = S20E("x") + return nil +} + +type S20 []S20E + +func (s *S20) UnmarshalText(text []byte) error { + *s = S20{"a", "b", "c"} + return nil +} + +// Test to ensure that when a custom type based on a slice implements an +// encoding.TextUnmarshaler interface that it takes precedence over any +// implementations by its elements. +func TestTextUnmarshalerTypeSlice(t *testing.T) { + data := map[string][]string{ + "Value": []string{"a,b,c"}, + } + s := struct { + Value S20 + }{} + decoder := NewDecoder() + if err := decoder.Decode(&s, data); err != nil { + t.Fatal("Error while decoding:", err) + } + expected := S20{"a", "b", "c"} + if !reflect.DeepEqual(expected, s.Value) { + t.Errorf("Expected %v errors, got %v", expected, s.Value) + } +} + +type S21E struct{ ElementValue string } + +func (e *S21E) UnmarshalText(text []byte) error { + *e = S21E{"x"} + return nil +} + +type S21 []S21E + +func (s *S21) UnmarshalText(text []byte) error { + *s = S21{{"a"}} + return nil +} + +type S21B []S21E + +// Test to ensure that if custom type base on a slice of structs implements an +// encoding.TextUnmarshaler interface it is unaffected by the special path +// requirements imposed on a slice of structs. +func TestTextUnmarshalerTypeSliceOfStructs(t *testing.T) { + data := map[string][]string{ + "Value": []string{"raw a"}, + } + // Implements encoding.TextUnmarshaler, should not throw invalid path + // error. + s := struct { + Value S21 + }{} + decoder := NewDecoder() + if err := decoder.Decode(&s, data); err != nil { + t.Fatal("Error while decoding:", err) + } + expected := S21{{"a"}} + if !reflect.DeepEqual(expected, s.Value) { + t.Errorf("Expected %v errors, got %v", expected, s.Value) + } + // Does not implement encoding.TextUnmarshaler, should throw invalid + // path error. + sb := struct { + Value S21B + }{} + if err := decoder.Decode(&sb, data); err == invalidPath { + t.Fatal("Expecting invalid path error", err) + } +} + +type S22 string + +func (s *S22) UnmarshalText(text []byte) error { + *s = S22("a") + return nil +} + +// Test to ensure that when a field that should be decoded into a type +// implementing the encoding.TextUnmarshaler interface is set to an empty value +// that the UnmarshalText method is utilized over other methods of decoding, +// especially including simply setting the zero value. +func TestTextUnmarshalerEmpty(t *testing.T) { + data := map[string][]string{ + "Value": []string{""}, // empty value + } + // Implements encoding.TextUnmarshaler, should use the type's + // UnmarshalText method. + s := struct { + Value S22 + }{} + decoder := NewDecoder() + if err := decoder.Decode(&s, data); err != nil { + t.Fatal("Error while decoding:", err) + } + expected := S22("a") + if expected != s.Value { + t.Errorf("Expected %v errors, got %v", expected, s.Value) + } +} + +type S23n struct { + F2 string `schema:"F2"` + F3 string `schema:"F3"` +} + +type S23e struct { + *S23n + F1 string `schema:"F1"` +} + +type S23 []*S23e + +func TestUnmashalPointerToEmbedded(t *testing.T) { + data := map[string][]string{ + "A.0.F2": []string{"raw a"}, + "A.0.F3": []string{"raw b"}, + } + + // Implements encoding.TextUnmarshaler, should not throw invalid path + // error. + s := struct { + Value S23 `schema:"A"` + }{} + decoder := NewDecoder() + + if err := decoder.Decode(&s, data); err != nil { + t.Fatal("Error while decoding:", err) + } + + expected := S23{ + &S23e{ + S23n: &S23n{"raw a", "raw b"}, + }, + } + if !reflect.DeepEqual(expected, s.Value) { + t.Errorf("Expected %v errors, got %v", expected, s.Value) + } +} diff --git a/utils/schema/doc.go b/utils/schema/doc.go new file mode 100644 index 00000000..aae9f33f --- /dev/null +++ b/utils/schema/doc.go @@ -0,0 +1,148 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package gorilla/schema fills a struct with form values. + +The basic usage is really simple. Given this struct: + + type Person struct { + Name string + Phone string + } + +...we can fill it passing a map to the Decode() function: + + values := map[string][]string{ + "Name": {"John"}, + "Phone": {"999-999-999"}, + } + person := new(Person) + decoder := schema.NewDecoder() + decoder.Decode(person, values) + +This is just a simple example and it doesn't make a lot of sense to create +the map manually. Typically it will come from a http.Request object and +will be of type url.Values, http.Request.Form, or http.Request.MultipartForm: + + func MyHandler(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + + if err != nil { + // Handle error + } + + decoder := schema.NewDecoder() + // r.PostForm is a map of our POST form values + err := decoder.Decode(person, r.PostForm) + + if err != nil { + // Handle error + } + + // Do something with person.Name or person.Phone + } + +Note: it is a good idea to set a Decoder instance as a package global, +because it caches meta-data about structs, and an instance can be shared safely: + + var decoder = schema.NewDecoder() + +To define custom names for fields, use a struct tag "schema". To not populate +certain fields, use a dash for the name and it will be ignored: + + type Person struct { + Name string `schema:"name"` // custom name + Phone string `schema:"phone"` // custom name + Admin bool `schema:"-"` // this field is never set + } + +The supported field types in the destination struct are: + + * bool + * float variants (float32, float64) + * int variants (int, int8, int16, int32, int64) + * string + * uint variants (uint, uint8, uint16, uint32, uint64) + * struct + * a pointer to one of the above types + * a slice or a pointer to a slice of one of the above types + +Non-supported types are simply ignored, however custom types can be registered +to be converted. + +To fill nested structs, keys must use a dotted notation as the "path" for the +field. So for example, to fill the struct Person below: + + type Phone struct { + Label string + Number string + } + + type Person struct { + Name string + Phone Phone + } + +...the source map must have the keys "Name", "Phone.Label" and "Phone.Number". +This means that an HTML form to fill a Person struct must look like this: + +
+ + + +
+ +Single values are filled using the first value for a key from the source map. +Slices are filled using all values for a key from the source map. So to fill +a Person with multiple Phone values, like: + + type Person struct { + Name string + Phones []Phone + } + +...an HTML form that accepts three Phone values would look like this: + +
+ + + + + + + +
+ +Notice that only for slices of structs the slice index is required. +This is needed for disambiguation: if the nested struct also had a slice +field, we could not translate multiple values to it if we did not use an +index for the parent struct. + +There's also the possibility to create a custom type that implements the +TextUnmarshaler interface, and in this case there's no need to register +a converter, like: + + type Person struct { + Emails []Email + } + + type Email struct { + *mail.Address + } + + func (e *Email) UnmarshalText(text []byte) (err error) { + e.Address, err = mail.ParseAddress(string(text)) + return + } + +...an HTML form that accepts three Email values would look like this: + +
+ + + +
+*/ +package schema diff --git a/utils/schema/encoder.go b/utils/schema/encoder.go new file mode 100644 index 00000000..f0ed6312 --- /dev/null +++ b/utils/schema/encoder.go @@ -0,0 +1,202 @@ +package schema + +import ( + "errors" + "fmt" + "reflect" + "strconv" +) + +type encoderFunc func(reflect.Value) string + +// Encoder encodes values from a struct into url.Values. +type Encoder struct { + cache *cache + regenc map[reflect.Type]encoderFunc +} + +// NewEncoder returns a new Encoder with defaults. +func NewEncoder() *Encoder { + return &Encoder{cache: newCache(), regenc: make(map[reflect.Type]encoderFunc)} +} + +// Encode encodes a struct into map[string][]string. +// +// Intended for use with url.Values. +func (e *Encoder) Encode(src interface{}, dst map[string][]string) error { + v := reflect.ValueOf(src) + + return e.encode(v, dst) +} + +// RegisterEncoder registers a converter for encoding a custom type. +func (e *Encoder) RegisterEncoder(value interface{}, encoder func(reflect.Value) string) { + e.regenc[reflect.TypeOf(value)] = encoder +} + +// SetAliasTag changes the tag used to locate custom field aliases. +// The default tag is "schema". +func (e *Encoder) SetAliasTag(tag string) { + e.cache.tag = tag +} + +// isValidStructPointer test if input value is a valid struct pointer. +func isValidStructPointer(v reflect.Value) bool { + return v.Type().Kind() == reflect.Ptr && v.Elem().IsValid() && v.Elem().Type().Kind() == reflect.Struct +} + +func isZero(v reflect.Value) bool { + switch v.Kind() { + case reflect.Func: + case reflect.Map, reflect.Slice: + return v.IsNil() || v.Len() == 0 + case reflect.Array: + z := true + for i := 0; i < v.Len(); i++ { + z = z && isZero(v.Index(i)) + } + return z + case reflect.Struct: + type zero interface { + IsZero() bool + } + if v.Type().Implements(reflect.TypeOf((*zero)(nil)).Elem()) { + iz := v.MethodByName("IsZero").Call([]reflect.Value{})[0] + return iz.Interface().(bool) + } + z := true + for i := 0; i < v.NumField(); i++ { + z = z && isZero(v.Field(i)) + } + return z + } + // Compare other types directly: + z := reflect.Zero(v.Type()) + return v.Interface() == z.Interface() +} + +func (e *Encoder) encode(v reflect.Value, dst map[string][]string) error { + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + if v.Kind() != reflect.Struct { + return errors.New("schema: interface must be a struct") + } + t := v.Type() + + errors := MultiError{} + + for i := 0; i < v.NumField(); i++ { + name, opts := fieldAlias(t.Field(i), e.cache.tag) + if name == "-" { + continue + } + + // Encode struct pointer types if the field is a valid pointer and a struct. + if isValidStructPointer(v.Field(i)) { + e.encode(v.Field(i).Elem(), dst) + continue + } + + encFunc := typeEncoder(v.Field(i).Type(), e.regenc) + + // Encode non-slice types and custom implementations immediately. + if encFunc != nil { + value := encFunc(v.Field(i)) + if opts.Contains("omitempty") && isZero(v.Field(i)) { + continue + } + + dst[name] = append(dst[name], value) + continue + } + + if v.Field(i).Type().Kind() == reflect.Struct { + e.encode(v.Field(i), dst) + continue + } + + if v.Field(i).Type().Kind() == reflect.Slice { + encFunc = typeEncoder(v.Field(i).Type().Elem(), e.regenc) + } + + if encFunc == nil { + errors[v.Field(i).Type().String()] = fmt.Errorf("schema: encoder not found for %v", v.Field(i)) + continue + } + + // Encode a slice. + if v.Field(i).Len() == 0 && opts.Contains("omitempty") { + continue + } + + dst[name] = []string{} + for j := 0; j < v.Field(i).Len(); j++ { + dst[name] = append(dst[name], encFunc(v.Field(i).Index(j))) + } + } + + if len(errors) > 0 { + return errors + } + return nil +} + +func typeEncoder(t reflect.Type, reg map[reflect.Type]encoderFunc) encoderFunc { + if f, ok := reg[t]; ok { + return f + } + + switch t.Kind() { + case reflect.Bool: + return encodeBool + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return encodeInt + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return encodeUint + case reflect.Float32: + return encodeFloat32 + case reflect.Float64: + return encodeFloat64 + case reflect.Ptr: + f := typeEncoder(t.Elem(), reg) + return func(v reflect.Value) string { + if v.IsNil() { + return "null" + } + return f(v.Elem()) + } + case reflect.String: + return encodeString + default: + return nil + } +} + +func encodeBool(v reflect.Value) string { + return strconv.FormatBool(v.Bool()) +} + +func encodeInt(v reflect.Value) string { + return strconv.FormatInt(int64(v.Int()), 10) +} + +func encodeUint(v reflect.Value) string { + return strconv.FormatUint(uint64(v.Uint()), 10) +} + +func encodeFloat(v reflect.Value, bits int) string { + return strconv.FormatFloat(v.Float(), 'f', 6, bits) +} + +func encodeFloat32(v reflect.Value) string { + return encodeFloat(v, 32) +} + +func encodeFloat64(v reflect.Value) string { + return encodeFloat(v, 64) +} + +func encodeString(v reflect.Value) string { + return v.String() +} diff --git a/utils/schema/encoder_test.go b/utils/schema/encoder_test.go new file mode 100644 index 00000000..368103b2 --- /dev/null +++ b/utils/schema/encoder_test.go @@ -0,0 +1,465 @@ +package schema + +import ( + "fmt" + "reflect" + "testing" + "time" +) + +type E1 struct { + F01 int `schema:"f01"` + F02 int `schema:"-"` + F03 string `schema:"f03"` + F04 string `schema:"f04,omitempty"` + F05 bool `schema:"f05"` + F06 bool `schema:"f06"` + F07 *string `schema:"f07"` + F08 *int8 `schema:"f08"` + F09 float64 `schema:"f09"` + F10 func() `schema:"f10"` + F11 inner +} +type inner struct { + F12 int +} + +func TestFilled(t *testing.T) { + f07 := "seven" + var f08 int8 = 8 + s := &E1{ + F01: 1, + F02: 2, + F03: "three", + F04: "four", + F05: true, + F06: false, + F07: &f07, + F08: &f08, + F09: 1.618, + F10: func() {}, + F11: inner{12}, + } + + vals := make(map[string][]string) + errs := NewEncoder().Encode(s, vals) + + valExists(t, "f01", "1", vals) + valNotExists(t, "f02", vals) + valExists(t, "f03", "three", vals) + valExists(t, "f05", "true", vals) + valExists(t, "f06", "false", vals) + valExists(t, "f07", "seven", vals) + valExists(t, "f08", "8", vals) + valExists(t, "f09", "1.618000", vals) + valExists(t, "F12", "12", vals) + + emptyErr := MultiError{} + if errs.Error() == emptyErr.Error() { + t.Errorf("Expected error got %v", errs) + } +} + +type Aa int + +type E3 struct { + F01 bool `schema:"f01"` + F02 float32 `schema:"f02"` + F03 float64 `schema:"f03"` + F04 int `schema:"f04"` + F05 int8 `schema:"f05"` + F06 int16 `schema:"f06"` + F07 int32 `schema:"f07"` + F08 int64 `schema:"f08"` + F09 string `schema:"f09"` + F10 uint `schema:"f10"` + F11 uint8 `schema:"f11"` + F12 uint16 `schema:"f12"` + F13 uint32 `schema:"f13"` + F14 uint64 `schema:"f14"` + F15 Aa `schema:"f15"` +} + +// Test compatibility with default decoder types. +func TestCompat(t *testing.T) { + src := &E3{ + F01: true, + F02: 4.2, + F03: 4.3, + F04: -42, + F05: -43, + F06: -44, + F07: -45, + F08: -46, + F09: "foo", + F10: 42, + F11: 43, + F12: 44, + F13: 45, + F14: 46, + F15: 1, + } + dst := &E3{} + + vals := make(map[string][]string) + encoder := NewEncoder() + decoder := NewDecoder() + + encoder.RegisterEncoder(src.F15, func(reflect.Value) string { return "1" }) + decoder.RegisterConverter(src.F15, func(string) reflect.Value { return reflect.ValueOf(1) }) + + err := encoder.Encode(src, vals) + if err != nil { + t.Errorf("Encoder has non-nil error: %v", err) + } + err = decoder.Decode(dst, vals) + if err != nil { + t.Errorf("Decoder has non-nil error: %v", err) + } + + if *src != *dst { + t.Errorf("Decoder-Encoder compatibility: expected %v, got %v\n", src, dst) + } +} + +func TestEmpty(t *testing.T) { + s := &E1{ + F01: 1, + F02: 2, + F03: "three", + } + + estr := "schema: encoder not found for " + vals := make(map[string][]string) + err := NewEncoder().Encode(s, vals) + if err.Error() != estr { + t.Errorf("Expected: %s, got %v", estr, err) + } + + valExists(t, "f03", "three", vals) + valNotExists(t, "f04", vals) +} + +func TestStruct(t *testing.T) { + estr := "schema: interface must be a struct" + vals := make(map[string][]string) + err := NewEncoder().Encode("hello world", vals) + + if err.Error() != estr { + t.Errorf("Expected: %s, got %v", estr, err) + } +} + +func TestSlices(t *testing.T) { + type oneAsWord int + ones := []oneAsWord{1, 2} + s1 := &struct { + ones []oneAsWord `schema:"ones"` + ints []int `schema:"ints"` + nonempty []int `schema:"nonempty"` + empty []int `schema:"empty,omitempty"` + }{ones, []int{1, 1}, []int{}, []int{}} + vals := make(map[string][]string) + + encoder := NewEncoder() + encoder.RegisterEncoder(ones[0], func(v reflect.Value) string { return "one" }) + err := encoder.Encode(s1, vals) + if err != nil { + t.Errorf("Encoder has non-nil error: %v", err) + } + + valsExist(t, "ones", []string{"one", "one"}, vals) + valsExist(t, "ints", []string{"1", "1"}, vals) + valsExist(t, "nonempty", []string{}, vals) + valNotExists(t, "empty", vals) +} + +func TestCompatSlices(t *testing.T) { + type oneAsWord int + type s1 struct { + Ones []oneAsWord `schema:"ones"` + Ints []int `schema:"ints"` + } + ones := []oneAsWord{1, 1} + src := &s1{ones, []int{1, 1}} + vals := make(map[string][]string) + dst := &s1{} + + encoder := NewEncoder() + encoder.RegisterEncoder(ones[0], func(v reflect.Value) string { return "one" }) + + decoder := NewDecoder() + decoder.RegisterConverter(ones[0], func(s string) reflect.Value { + if s == "one" { + return reflect.ValueOf(1) + } + return reflect.ValueOf(2) + }) + + err := encoder.Encode(src, vals) + if err != nil { + t.Errorf("Encoder has non-nil error: %v", err) + } + err = decoder.Decode(dst, vals) + if err != nil { + t.Errorf("Dncoder has non-nil error: %v", err) + } + + if len(src.Ints) != len(dst.Ints) || len(src.Ones) != len(src.Ones) { + t.Fatalf("Expected %v, got %v", src, dst) + } + + for i, v := range src.Ones { + if dst.Ones[i] != v { + t.Fatalf("Expected %v, got %v", v, dst.Ones[i]) + } + } + + for i, v := range src.Ints { + if dst.Ints[i] != v { + t.Fatalf("Expected %v, got %v", v, dst.Ints[i]) + } + } +} + +func TestRegisterEncoder(t *testing.T) { + type oneAsWord int + type twoAsWord int + type oneSliceAsWord []int + + s1 := &struct { + oneAsWord + twoAsWord + oneSliceAsWord + }{1, 2, []int{1, 1}} + v1 := make(map[string][]string) + + encoder := NewEncoder() + encoder.RegisterEncoder(s1.oneAsWord, func(v reflect.Value) string { return "one" }) + encoder.RegisterEncoder(s1.twoAsWord, func(v reflect.Value) string { return "two" }) + encoder.RegisterEncoder(s1.oneSliceAsWord, func(v reflect.Value) string { return "one" }) + + err := encoder.Encode(s1, v1) + if err != nil { + t.Errorf("Encoder has non-nil error: %v", err) + } + + valExists(t, "oneAsWord", "one", v1) + valExists(t, "twoAsWord", "two", v1) + valExists(t, "oneSliceAsWord", "one", v1) +} + +func TestEncoderOrder(t *testing.T) { + type builtinEncoderSimple int + type builtinEncoderSimpleOverridden int + type builtinEncoderSlice []int + type builtinEncoderSliceOverridden []int + type builtinEncoderStruct struct{ nr int } + type builtinEncoderStructOverridden struct{ nr int } + + s1 := &struct { + builtinEncoderSimple `schema:"simple"` + builtinEncoderSimpleOverridden `schema:"simple_overridden"` + builtinEncoderSlice `schema:"slice"` + builtinEncoderSliceOverridden `schema:"slice_overridden"` + builtinEncoderStruct `schema:"struct"` + builtinEncoderStructOverridden `schema:"struct_overridden"` + }{ + 1, + 1, + []int{2}, + []int{2}, + builtinEncoderStruct{3}, + builtinEncoderStructOverridden{3}, + } + v1 := make(map[string][]string) + + encoder := NewEncoder() + encoder.RegisterEncoder(s1.builtinEncoderSimpleOverridden, func(v reflect.Value) string { return "one" }) + encoder.RegisterEncoder(s1.builtinEncoderSliceOverridden, func(v reflect.Value) string { return "two" }) + encoder.RegisterEncoder(s1.builtinEncoderStructOverridden, func(v reflect.Value) string { return "three" }) + + err := encoder.Encode(s1, v1) + if err != nil { + t.Errorf("Encoder has non-nil error: %v", err) + } + + valExists(t, "simple", "1", v1) + valExists(t, "simple_overridden", "one", v1) + valExists(t, "slice", "2", v1) + valExists(t, "slice_overridden", "two", v1) + valExists(t, "nr", "3", v1) + valExists(t, "struct_overridden", "three", v1) +} + +func valExists(t *testing.T, key string, expect string, result map[string][]string) { + valsExist(t, key, []string{expect}, result) +} + +func valsExist(t *testing.T, key string, expect []string, result map[string][]string) { + vals, ok := result[key] + if !ok { + t.Fatalf("Key not found. Expected: %s", key) + } + + if len(expect) != len(vals) { + t.Fatalf("Expected: %v, got: %v", expect, vals) + } + + for i, v := range expect { + if vals[i] != v { + t.Fatalf("Unexpected value. Expected: %v, got %v", v, vals[i]) + } + } +} + +func valNotExists(t *testing.T, key string, result map[string][]string) { + if val, ok := result[key]; ok { + t.Error("Key not ommited. Expected: empty; got: " + val[0] + ".") + } +} + +type E4 struct { + ID string `json:"id"` +} + +func TestEncoderSetAliasTag(t *testing.T) { + data := map[string][]string{} + + s := E4{ + ID: "foo", + } + encoder := NewEncoder() + encoder.SetAliasTag("json") + encoder.Encode(&s, data) + valExists(t, "id", "foo", data) +} + +type E5 struct { + F01 int `schema:"f01,omitempty"` + F02 string `schema:"f02,omitempty"` + F03 *string `schema:"f03,omitempty"` + F04 *int8 `schema:"f04,omitempty"` + F05 float64 `schema:"f05,omitempty"` + F06 E5F06 `schema:"f06,omitempty"` + F07 E5F06 `schema:"f07,omitempty"` + F08 []string `schema:"f08,omitempty"` + F09 []string `schema:"f09,omitempty"` +} + +type E5F06 struct { + F0601 string `schema:"f0601,omitempty"` +} + +func TestEncoderWithOmitempty(t *testing.T) { + vals := map[string][]string{} + + s := E5{ + F02: "test", + F07: E5F06{ + F0601: "test", + }, + F09: []string{"test"}, + } + + encoder := NewEncoder() + encoder.Encode(&s, vals) + + valNotExists(t, "f01", vals) + valExists(t, "f02", "test", vals) + valNotExists(t, "f03", vals) + valNotExists(t, "f04", vals) + valNotExists(t, "f05", vals) + valNotExists(t, "f06", vals) + valExists(t, "f0601", "test", vals) + valNotExists(t, "f08", vals) + valsExist(t, "f09", []string{"test"}, vals) +} + +type E6 struct { + F01 *inner + F02 *inner + F03 *inner `schema:",omitempty"` +} + +func TestStructPointer(t *testing.T) { + vals := map[string][]string{} + s := E6{ + F01: &inner{2}, + } + + encoder := NewEncoder() + encoder.Encode(&s, vals) + valExists(t, "F12", "2", vals) + valExists(t, "F02", "null", vals) + valNotExists(t, "F03", vals) +} + +func TestRegisterEncoderCustomArrayType(t *testing.T) { + type CustomInt []int + type S1 struct { + SomeInts CustomInt `schema:",omitempty"` + } + + ss := []S1{ + {}, + {CustomInt{}}, + {CustomInt{1, 2, 3}}, + } + + for s := range ss { + vals := map[string][]string{} + + encoder := NewEncoder() + encoder.RegisterEncoder(CustomInt{}, func(value reflect.Value) string { + return fmt.Sprint(value.Interface()) + }) + + encoder.Encode(s, vals) + } +} + +func TestRegisterEncoderStructIsZero(t *testing.T) { + type S1 struct { + SomeTime1 time.Time `schema:"tim1,omitempty"` + SomeTime2 time.Time `schema:"tim2,omitempty"` + } + + ss := []*S1{ + { + SomeTime1: time.Date(2020, 8, 4, 13, 30, 1, 0, time.UTC), + }, + } + + for s := range ss { + vals := map[string][]string{} + + encoder := NewEncoder() + encoder.RegisterEncoder(time.Time{}, func(value reflect.Value) string { + return value.Interface().(time.Time).Format(time.RFC3339Nano) + }) + + err := encoder.Encode(ss[s], vals) + if err != nil { + t.Errorf("Encoder has non-nil error: %v", err) + } + + ta, ok := vals["tim1"] + if !ok { + t.Error("expected tim1 to be present") + } + + if len(ta) != 1 { + t.Error("expected tim1 to be present") + } + + if "2020-08-04T13:30:01Z" != ta[0] { + t.Error("expected correct tim1 time") + } + + _, ok = vals["tim2"] + if ok { + t.Error("expected tim1 not to be present") + } + } +} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 00000000..09f240ed --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,505 @@ +// โšก๏ธ Fiber is an Express inspired web framework written in Go with โ˜•๏ธ +// ๐Ÿค– Github Repository: https://github.com/gofiber/fiber +// ๐Ÿ“Œ API Documentation: https://docs.gofiber.io + +package utils + +import ( + "bytes" + "crypto/rand" + "encoding/binary" + "encoding/hex" + "fmt" + "log" + "path/filepath" + "reflect" + "runtime" + "strings" + "sync/atomic" + "testing" + "text/tabwriter" + "unsafe" +) + +const toLowerTable = "\x00\x01\x02\x03\x04\x05\x06\a\b\t\n\v\f\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>?@abcdefghijklmnopqrstuvwxyz[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u007f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff" +const toUpperTable = "\x00\x01\x02\x03\x04\x05\x06\a\b\t\n\v\f\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`ABCDEFGHIJKLMNOPQRSTUVWXYZ{|}~\u007f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff" + +// Copyright ยฉ 2014, Roger Peppe +// github.com/rogpeppe/fastuuid +// All rights reserved. + +var uuidSeed [24]byte +var uuidCounter uint64 + +func UUID() string { + // Setup seed & counter once + if uuidCounter <= 0 { + if _, err := rand.Read(uuidSeed[:]); err != nil { + return "00000000-0000-0000-0000-000000000000" + } + uuidCounter = binary.LittleEndian.Uint64(uuidSeed[:8]) + } + // first 8 bytes differ, taking a slice of the first 16 bytes + x := atomic.AddUint64(&uuidCounter, 1) + uuid := uuidSeed + binary.LittleEndian.PutUint64(uuid[:8], x) + uuid[6], uuid[9] = uuid[9], uuid[6] + + // RFC4122 v4 + uuid[6] = (uuid[6] & 0x0f) | 0x40 + uuid[8] = uuid[8]&0x3f | 0x80 + + // create UUID representation of the first 128 bits + b := make([]byte, 36) + hex.Encode(b[0:8], uuid[0:4]) + b[8] = '-' + hex.Encode(b[9:13], uuid[4:6]) + b[13] = '-' + hex.Encode(b[14:18], uuid[6:8]) + b[18] = '-' + hex.Encode(b[19:23], uuid[8:10]) + b[23] = '-' + hex.Encode(b[24:], uuid[10:16]) + + return GetString(b) +} + +// Returns function name +func FunctionName(fn interface{}) string { + t := reflect.ValueOf(fn).Type() + if t.Kind() == reflect.Func { + return runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name() + } + return t.String() +} + +// ToLower is the equivalent of strings.ToLower +func ToLower(b string) string { + var res = make([]byte, len(b)) + copy(res, b) + for i := 0; i < len(res); i++ { + res[i] = toLowerTable[res[i]] + } + + return GetString(res) +} + +// ToLowerBytes is the equivalent of bytes.ToLower +func ToLowerBytes(b []byte) []byte { + for i := 0; i < len(b); i++ { + b[i] = toLowerTable[b[i]] + } + return b +} + +// ToUpper is the equivalent of strings.ToUpper +func ToUpper(b string) string { + var res = make([]byte, len(b)) + copy(res, b) + for i := 0; i < len(res); i++ { + res[i] = toUpperTable[res[i]] + } + + return GetString(res) +} + +// ToUpperBytes is the equivalent of bytes.ToUpper +func ToUpperBytes(b []byte) []byte { + for i := 0; i < len(b); i++ { + b[i] = toUpperTable[b[i]] + } + return b +} + +// TrimRight is the equivalent of strings.TrimRight +func TrimRight(s string, cutset byte) string { + lenStr := len(s) + for lenStr > 0 && s[lenStr-1] == cutset { + lenStr-- + } + return s[:lenStr] +} + +// TrimRightBytes is the equivalent of bytes.TrimRight +func TrimRightBytes(b []byte, cutset byte) []byte { + lenStr := len(b) + for lenStr > 0 && b[lenStr-1] == cutset { + lenStr-- + } + return b[:lenStr] +} + +// TrimLeft is the equivalent of strings.TrimLeft +func TrimLeft(s string, cutset byte) string { + lenStr, start := len(s), 0 + for start < lenStr && s[start] == cutset { + start++ + } + return s[start:] +} + +// TrimLeftBytes is the equivalent of bytes.TrimLeft +func TrimLeftBytes(b []byte, cutset byte) []byte { + lenStr, start := len(b), 0 + for start < lenStr && b[start] == cutset { + start++ + } + return b[start:] +} + +// Trim is the equivalent of strings.Trim +func Trim(s string, cutset byte) string { + i, j := 0, len(s)-1 + for ; i < j; i++ { + if s[i] != cutset { + break + } + } + for ; i < j; j-- { + if s[j] != cutset { + break + } + } + + return s[i : j+1] +} + +// TrimBytes is the equivalent of bytes.Trim +func TrimBytes(b []byte, cutset byte) []byte { + i, j := 0, len(b)-1 + for ; i < j; i++ { + if b[i] != cutset { + break + } + } + for ; i < j; j-- { + if b[j] != cutset { + break + } + } + + return b[i : j+1] +} + +// EqualFold the equivalent of bytes.EqualFold +func EqualsFold(b, s []byte) (equals bool) { + n := len(b) + equals = n == len(s) + if equals { + for i := 0; i < n; i++ { + if equals = b[i]|0x20 == s[i]|0x20; !equals { + break + } + } + } + return +} + +// #nosec G103 +// GetString returns a string pointer without allocation +func GetString(b []byte) string { + return *(*string)(unsafe.Pointer(&b)) +} + +// #nosec G103 +// GetBytes returns a byte pointer without allocation +func GetBytes(s string) (bs []byte) { + sh := (*reflect.StringHeader)(unsafe.Pointer(&s)) + bh := (*reflect.SliceHeader)(unsafe.Pointer(&bs)) + bh.Data = sh.Data + bh.Len = sh.Len + bh.Cap = sh.Len + return +} + +// ImmutableString copies a string to make it immutable +func ImmutableString(s string) string { + return string(GetBytes(s)) +} + +// AssertEqual checks if values are equal +func AssertEqual(t testing.TB, expected interface{}, actual interface{}, description ...string) { + if reflect.DeepEqual(expected, actual) { + return + } + var aType = "" + var bType = "" + if reflect.ValueOf(expected).IsValid() { + aType = reflect.TypeOf(expected).Name() + } + if reflect.ValueOf(actual).IsValid() { + bType = reflect.TypeOf(actual).Name() + } + + testName := "AssertEqual" + if t != nil { + testName = t.Name() + } + + _, file, line, _ := runtime.Caller(1) + + var buf bytes.Buffer + w := tabwriter.NewWriter(&buf, 0, 0, 5, ' ', 0) + fmt.Fprintf(w, "\nTest:\t%s", testName) + fmt.Fprintf(w, "\nTrace:\t%s:%d", filepath.Base(file), line) + fmt.Fprintf(w, "\nError:\tNot equal") + fmt.Fprintf(w, "\nExpect:\t%v\t[%s]", expected, aType) + fmt.Fprintf(w, "\nResult:\t%v\t[%s]", actual, bType) + + if len(description) > 0 { + fmt.Fprintf(w, "\nDescription:\t%s", description[0]) + } + + result := "" + if err := w.Flush(); err != nil { + result = err.Error() + } else { + result = buf.String() + } + if t != nil { + t.Fatal(result) + } else { + log.Fatal(result) + } +} + +const MIMEOctetStream = "application/octet-stream" + +// GetMIME returns the content-type of a file extension +func GetMIME(extension string) (mime string) { + if len(extension) == 0 { + return mime + } + if extension[0] == '.' { + mime = mimeExtensions[extension[1:]] + } else { + mime = mimeExtensions[extension] + } + if len(mime) == 0 { + return MIMEOctetStream + } + return mime +} + +// GetTrimmedParam trims the ':' & '?' from a string +func GetTrimmedParam(param string) string { + start := 0 + end := len(param) + + if param[start] != ':' { // is not a param + return param + } + start++ + if param[end-1] == '?' { // is ? + end-- + } + + return param[start:end] +} + +// GetCharPos ... +func GetCharPos(s string, char byte, matchCount int) int { + if matchCount == 0 { + matchCount = 1 + } + endPos, pos := 0, -2 + for matchCount > 0 && pos != -1 { + if pos > -1 { + s = s[pos+1:] + endPos++ + } + pos = strings.IndexByte(s, char) + endPos += pos + matchCount-- + } + return endPos +} + +// limits for HTTP statuscodes +const ( + statusMessageMin = 100 + statusMessageMax = 511 +) + +// StatusMessage returns the correct message for the provided HTTP statuscode +func StatusMessage(status int) string { + if status < statusMessageMin || status > statusMessageMax { + return "" + } + return statusMessage[status] +} + +// HTTP status codes were copied from net/http. +var statusMessage = []string{ + 100: "Continue", + 101: "Switching Protocols", + 102: "Processing", + 103: "Early Hints", + 200: "OK", + 201: "Created", + 202: "Accepted", + 203: "Non-Authoritative Information", + 204: "No Content", + 205: "Reset Content", + 206: "Partial Content", + 207: "Multi-Status", + 208: "Already Reported", + 226: "IM Used", + 300: "Multiple Choices", + 301: "Moved Permanently", + 302: "Found", + 303: "See Other", + 304: "Not Modified", + 305: "Use Proxy", + 306: "Switch Proxy", + 307: "Temporary Redirect", + 308: "Permanent Redirect", + 400: "Bad Request", + 401: "Unauthorized", + 402: "Payment Required", + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 406: "Not Acceptable", + 407: "Proxy Authentication Required", + 408: "Request Timeout", + 409: "Conflict", + 410: "Gone", + 411: "Length Required", + 412: "Precondition Failed", + 413: "Request Entity Too Large", + 414: "Request URI Too Long", + 415: "Unsupported Media Type", + 416: "Requested Range Not Satisfiable", + 417: "Expectation Failed", + 418: "I'm a teapot", + 421: "Misdirected Request", + 422: "Unprocessable Entity", + 423: "Locked", + 424: "Failed Dependency", + 426: "Upgrade Required", + 428: "Precondition Required", + 429: "Too Many Requests", + 431: "Request Header Fields Too Large", + 451: "Unavailable For Legal Reasons", + 500: "Internal Server Error", + 501: "Not Implemented", + 502: "Bad Gateway", + 503: "Service Unavailable", + 504: "Gateway Timeout", + 505: "HTTP Version Not Supported", + 506: "Variant Also Negotiates", + 507: "Insufficient Storage", + 508: "Loop Detected", + 510: "Not Extended", + 511: "Network Authentication Required", +} + +// MIME types were copied from https://github.com/nginx/nginx/blob/master/conf/mime.types +var mimeExtensions = map[string]string{ + "html": "text/html", + "htm": "text/html", + "shtml": "text/html", + "css": "text/css", + "gif": "image/gif", + "jpeg": "image/jpeg", + "jpg": "image/jpeg", + "xml": "application/xml", + "js": "application/javascript", + "atom": "application/atom+xml", + "rss": "application/rss+xml", + "mml": "text/mathml", + "txt": "text/plain", + "jad": "text/vnd.sun.j2me.app-descriptor", + "wml": "text/vnd.wap.wml", + "htc": "text/x-component", + "png": "image/png", + "svg": "image/svg+xml", + "svgz": "image/svg+xml", + "tif": "image/tiff", + "tiff": "image/tiff", + "wbmp": "image/vnd.wap.wbmp", + "webp": "image/webp", + "ico": "image/x-icon", + "jng": "image/x-jng", + "bmp": "image/x-ms-bmp", + "woff": "font/woff", + "woff2": "font/woff2", + "jar": "application/java-archive", + "war": "application/java-archive", + "ear": "application/java-archive", + "json": "application/json", + "hqx": "application/mac-binhex40", + "doc": "application/msword", + "pdf": "application/pdf", + "ps": "application/postscript", + "eps": "application/postscript", + "ai": "application/postscript", + "rtf": "application/rtf", + "m3u8": "application/vnd.apple.mpegurl", + "kml": "application/vnd.google-earth.kml+xml", + "kmz": "application/vnd.google-earth.kmz", + "xls": "application/vnd.ms-excel", + "eot": "application/vnd.ms-fontobject", + "ppt": "application/vnd.ms-powerpoint", + "odg": "application/vnd.oasis.opendocument.graphics", + "odp": "application/vnd.oasis.opendocument.presentation", + "ods": "application/vnd.oasis.opendocument.spreadsheet", + "odt": "application/vnd.oasis.opendocument.text", + "wmlc": "application/vnd.wap.wmlc", + "7z": "application/x-7z-compressed", + "cco": "application/x-cocoa", + "jardiff": "application/x-java-archive-diff", + "jnlp": "application/x-java-jnlp-file", + "run": "application/x-makeself", + "pl": "application/x-perl", + "pm": "application/x-perl", + "prc": "application/x-pilot", + "pdb": "application/x-pilot", + "rar": "application/x-rar-compressed", + "rpm": "application/x-redhat-package-manager", + "sea": "application/x-sea", + "swf": "application/x-shockwave-flash", + "sit": "application/x-stuffit", + "tcl": "application/x-tcl", + "tk": "application/x-tcl", + "der": "application/x-x509-ca-cert", + "pem": "application/x-x509-ca-cert", + "crt": "application/x-x509-ca-cert", + "xpi": "application/x-xpinstall", + "xhtml": "application/xhtml+xml", + "xspf": "application/xspf+xml", + "zip": "application/zip", + "bin": "application/octet-stream", + "exe": "application/octet-stream", + "dll": "application/octet-stream", + "deb": "application/octet-stream", + "dmg": "application/octet-stream", + "iso": "application/octet-stream", + "img": "application/octet-stream", + "msi": "application/octet-stream", + "msp": "application/octet-stream", + "msm": "application/octet-stream", + "mid": "audio/midi", + "midi": "audio/midi", + "kar": "audio/midi", + "mp3": "audio/mpeg", + "ogg": "audio/ogg", + "m4a": "audio/x-m4a", + "ra": "audio/x-realaudio", + "3gpp": "video/3gpp", + "3gp": "video/3gpp", + "ts": "video/mp2t", + "mp4": "video/mp4", + "mpeg": "video/mpeg", + "mpg": "video/mpeg", + "mov": "video/quicktime", + "webm": "video/webm", + "flv": "video/x-flv", + "m4v": "video/x-m4v", + "mng": "video/x-mng", + "asx": "video/x-ms-asf", + "asf": "video/x-ms-asf", + "wmv": "video/x-ms-wmv", + "avi": "video/x-msvideo", +} diff --git a/utils/utils_test.go b/utils/utils_test.go new file mode 100644 index 00000000..46ebcda7 --- /dev/null +++ b/utils/utils_test.go @@ -0,0 +1,502 @@ +// โšก๏ธ Fiber is an Express inspired web framework written in Go with โ˜•๏ธ +// ๐Ÿค– Github Repository: https://github.com/gofiber/fiber +// ๐Ÿ“Œ API Documentation: https://docs.gofiber.io + +package utils + +import ( + "bytes" + "crypto/rand" + "fmt" + "mime" + "net/http" + "strings" + "testing" +) + +func Test_Utils_FunctionName(t *testing.T) { + t.Parallel() + AssertEqual(t, "github.com/gofiber/fiber/v2/utils.Test_Utils_UUID", FunctionName(Test_Utils_UUID)) + + AssertEqual(t, "github.com/gofiber/fiber/v2/utils.Test_Utils_FunctionName.func1", FunctionName(func() {})) + + var dummyint = 20 + AssertEqual(t, "int", FunctionName(dummyint)) +} + +func Test_Utils_UUID(t *testing.T) { + t.Parallel() + res := UUID() + AssertEqual(t, 36, len(res)) +} + +// go test -v -run=^$ -bench=Benchmark_UUID -benchmem -count=2 + +func Benchmark_UUID(b *testing.B) { + var res string + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = UUID() + } + AssertEqual(b, 36, len(res)) + }) + b.Run("default", func(b *testing.B) { + rnd := make([]byte, 16) + _, _ = rand.Read(rnd) + for n := 0; n < b.N; n++ { + res = fmt.Sprintf("%x-%x-%x-%x-%x", rnd[0:4], rnd[4:6], rnd[6:8], rnd[8:10], rnd[10:]) + } + AssertEqual(b, 36, len(res)) + }) +} + +func Test_Utils_ToUpper(t *testing.T) { + t.Parallel() + res := ToUpper("/my/name/is/:param/*") + AssertEqual(t, "/MY/NAME/IS/:PARAM/*", res) +} + +func Benchmark_ToUpper(b *testing.B) { + var path = "/RePos/GoFiBer/FibEr/iSsues/187643/CoMmEnts" + var res string + + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = ToUpper(path) + } + AssertEqual(b, "/REPOS/GOFIBER/FIBER/ISSUES/187643/COMMENTS", res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = strings.ToUpper(path) + } + AssertEqual(b, "/REPOS/GOFIBER/FIBER/ISSUES/187643/COMMENTS", res) + }) +} + +func Test_Utils_ToLower(t *testing.T) { + t.Parallel() + res := ToLower("/MY/NAME/IS/:PARAM/*") + AssertEqual(t, "/my/name/is/:param/*", res) + res = ToLower("/MY1/NAME/IS/:PARAM/*") + AssertEqual(t, "/my1/name/is/:param/*", res) + res = ToLower("/MY2/NAME/IS/:PARAM/*") + AssertEqual(t, "/my2/name/is/:param/*", res) + res = ToLower("/MY3/NAME/IS/:PARAM/*") + AssertEqual(t, "/my3/name/is/:param/*", res) + res = ToLower("/MY4/NAME/IS/:PARAM/*") + AssertEqual(t, "/my4/name/is/:param/*", res) +} + +func Benchmark_ToLower(b *testing.B) { + var path = "/RePos/GoFiBer/FibEr/iSsues/187643/CoMmEnts" + var res string + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = ToLower(path) + } + AssertEqual(b, "/repos/gofiber/fiber/issues/187643/comments", res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = strings.ToLower(path) + } + AssertEqual(b, "/repos/gofiber/fiber/issues/187643/comments", res) + }) +} + +func Test_Utils_ToLowerBytes(t *testing.T) { + t.Parallel() + res := ToLowerBytes([]byte("/MY/NAME/IS/:PARAM/*")) + AssertEqual(t, true, bytes.Equal([]byte("/my/name/is/:param/*"), res)) + res = ToLowerBytes([]byte("/MY1/NAME/IS/:PARAM/*")) + AssertEqual(t, true, bytes.Equal([]byte("/my1/name/is/:param/*"), res)) + res = ToLowerBytes([]byte("/MY2/NAME/IS/:PARAM/*")) + AssertEqual(t, true, bytes.Equal([]byte("/my2/name/is/:param/*"), res)) + res = ToLowerBytes([]byte("/MY3/NAME/IS/:PARAM/*")) + AssertEqual(t, true, bytes.Equal([]byte("/my3/name/is/:param/*"), res)) + res = ToLowerBytes([]byte("/MY4/NAME/IS/:PARAM/*")) + AssertEqual(t, true, bytes.Equal([]byte("/my4/name/is/:param/*"), res)) +} + +func Benchmark_ToLowerBytes(b *testing.B) { + var path = []byte("/RePos/GoFiBer/FibEr/iSsues/187643/CoMmEnts") + var res []byte + + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = ToLowerBytes(path) + } + AssertEqual(b, bytes.EqualFold(GetBytes("/repos/gofiber/fiber/issues/187643/comments"), res), true) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = bytes.ToLower(path) + } + AssertEqual(b, bytes.EqualFold(GetBytes("/repos/gofiber/fiber/issues/187643/comments"), res), true) + }) +} + +func Benchmark_EqualFolds(b *testing.B) { + var left = []byte("/RePos/GoFiBer/FibEr/iSsues/187643/CoMmEnts") + var right = []byte("/RePos/goFiber/Fiber/issues/187643/COMMENTS") + var res bool + + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = EqualsFold(left, right) + } + AssertEqual(b, true, res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = bytes.EqualFold(left, right) + } + AssertEqual(b, true, res) + }) +} + +func Test_Utils_EqualsFold(t *testing.T) { + t.Parallel() + res := EqualsFold([]byte("/MY/NAME/IS/:PARAM/*"), []byte("/my/name/is/:param/*")) + AssertEqual(t, true, res) + res = EqualsFold([]byte("/MY1/NAME/IS/:PARAM/*"), []byte("/MY1/NAME/IS/:PARAM/*")) + AssertEqual(t, true, res) + res = EqualsFold([]byte("/my2/name/is/:param/*"), []byte("/my2/name")) + AssertEqual(t, false, res) + res = EqualsFold([]byte("/dddddd"), []byte("eeeeee")) + AssertEqual(t, false, res) + res = EqualsFold([]byte("/MY3/NAME/IS/:PARAM/*"), []byte("/my3/name/is/:param/*")) + AssertEqual(t, true, res) + res = EqualsFold([]byte("/MY4/NAME/IS/:PARAM/*"), []byte("/my4/nAME/IS/:param/*")) + AssertEqual(t, true, res) +} + +func Test_Utils_TrimRight(t *testing.T) { + t.Parallel() + res := TrimRight("/test//////", '/') + AssertEqual(t, "/test", res) + + res = TrimRight("/test", '/') + AssertEqual(t, "/test", res) +} +func Benchmark_TrimRight(b *testing.B) { + var res string + + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = TrimRight("foobar ", ' ') + } + AssertEqual(b, "foobar", res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = strings.TrimRight("foobar ", " ") + } + AssertEqual(b, "foobar", res) + }) +} + +func Test_Utils_TrimLeft(t *testing.T) { + t.Parallel() + res := TrimLeft("////test/", '/') + AssertEqual(t, "test/", res) + + res = TrimLeft("test/", '/') + AssertEqual(t, "test/", res) +} +func Benchmark_TrimLeft(b *testing.B) { + var res string + + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = TrimLeft(" foobar", ' ') + } + AssertEqual(b, "foobar", res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = strings.TrimLeft(" foobar", " ") + } + AssertEqual(b, "foobar", res) + }) +} +func Test_Utils_Trim(t *testing.T) { + t.Parallel() + res := Trim(" test ", ' ') + AssertEqual(t, "test", res) + + res = Trim("test", ' ') + AssertEqual(t, "test", res) + + res = Trim(".test", '.') + AssertEqual(t, "test", res) +} + +func Benchmark_Trim(b *testing.B) { + var res string + + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = Trim(" foobar ", ' ') + } + AssertEqual(b, "foobar", res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = strings.Trim(" foobar ", " ") + } + AssertEqual(b, "foobar", res) + }) + b.Run("default.trimspace", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = strings.TrimSpace(" foobar ") + } + AssertEqual(b, "foobar", res) + }) +} + +func Test_Utils_TrimRightBytes(t *testing.T) { + t.Parallel() + res := TrimRightBytes([]byte("/test//////"), '/') + AssertEqual(t, []byte("/test"), res) + + res = TrimRightBytes([]byte("/test"), '/') + AssertEqual(t, []byte("/test"), res) +} + +func Benchmark_TrimRightBytes(b *testing.B) { + var res []byte + + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = TrimRightBytes([]byte("foobar "), ' ') + } + AssertEqual(b, []byte("foobar"), res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = bytes.TrimRight([]byte("foobar "), " ") + } + AssertEqual(b, []byte("foobar"), res) + }) +} + +func Test_Utils_TrimLeftBytes(t *testing.T) { + t.Parallel() + res := TrimLeftBytes([]byte("////test/"), '/') + AssertEqual(t, []byte("test/"), res) + + res = TrimLeftBytes([]byte("test/"), '/') + AssertEqual(t, []byte("test/"), res) +} +func Benchmark_TrimLeftBytes(b *testing.B) { + var res []byte + + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = TrimLeftBytes([]byte(" foobar"), ' ') + } + AssertEqual(b, []byte("foobar"), res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = bytes.TrimLeft([]byte(" foobar"), " ") + } + AssertEqual(b, []byte("foobar"), res) + }) +} +func Test_Utils_TrimBytes(t *testing.T) { + t.Parallel() + res := TrimBytes([]byte(" test "), ' ') + AssertEqual(t, []byte("test"), res) + + res = TrimBytes([]byte("test"), ' ') + AssertEqual(t, []byte("test"), res) + + res = TrimBytes([]byte(".test"), '.') + AssertEqual(t, []byte("test"), res) +} +func Benchmark_TrimBytes(b *testing.B) { + var res []byte + + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = TrimBytes([]byte(" foobar "), ' ') + } + AssertEqual(b, []byte("foobar"), res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = bytes.Trim([]byte(" foobar "), " ") + } + AssertEqual(b, []byte("foobar"), res) + }) +} + +func Test_Utils_GetCharPos(t *testing.T) { + t.Parallel() + res := GetCharPos("/foo/bar/foobar/test", '/', 3) + AssertEqual(t, 8, res) + res = GetCharPos("foo/bar/foobar/test", '/', 3) + AssertEqual(t, 14, res) + res = GetCharPos("foo/bar/foobar/test", '/', 1) + AssertEqual(t, 3, res) + res = GetCharPos("foo/bar/foobar/test", 'f', 2) + AssertEqual(t, 8, res) + res = GetCharPos("foo/bar/foobar/test", 'f', 0) + AssertEqual(t, 0, res) +} + +func Test_Utils_GetTrimmedParam(t *testing.T) { + t.Parallel() + res := GetTrimmedParam("*") + AssertEqual(t, "*", res) + res = GetTrimmedParam(":param") + AssertEqual(t, "param", res) + res = GetTrimmedParam(":param1?") + AssertEqual(t, "param1", res) + res = GetTrimmedParam("noParam") + AssertEqual(t, "noParam", res) +} + +func Test_Utils_GetString(t *testing.T) { + t.Parallel() + res := GetString([]byte("Hello, World!")) + AssertEqual(t, "Hello, World!", res) +} + +// go test -v -run=^$ -bench=GetString -benchmem -count=2 + +func Benchmark_GetString(b *testing.B) { + var hello = []byte("Hello, World!") + var res string + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = GetString(hello) + } + AssertEqual(b, "Hello, World!", res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = string(hello) + } + AssertEqual(b, "Hello, World!", res) + }) +} + +func Test_Utils_GetBytes(t *testing.T) { + t.Parallel() + res := GetBytes("Hello, World!") + AssertEqual(t, []byte("Hello, World!"), res) +} + +// go test -v -run=^$ -bench=GetBytes -benchmem -count=4 + +func Benchmark_GetBytes(b *testing.B) { + var hello = "Hello, World!" + var res []byte + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = GetBytes(hello) + } + AssertEqual(b, []byte("Hello, World!"), res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = []byte(hello) + } + AssertEqual(b, []byte("Hello, World!"), res) + }) +} + +func Test_Utils_ImmutableString(t *testing.T) { + t.Parallel() + res := ImmutableString("Hello, World!") + AssertEqual(t, "Hello, World!", res) +} + +func Test_Utils_GetMIME(t *testing.T) { + t.Parallel() + res := GetMIME(".json") + AssertEqual(t, "application/json", res) + + res = GetMIME(".xml") + AssertEqual(t, "application/xml", res) + + res = GetMIME("xml") + AssertEqual(t, "application/xml", res) + + res = GetMIME("unknown") + AssertEqual(t, MIMEOctetStream, res) + // empty case + res = GetMIME("") + AssertEqual(t, "", res) +} + +// go test -v -run=^$ -bench=Benchmark_GetMIME -benchmem -count=2 +func Benchmark_GetMIME(b *testing.B) { + var res string + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = GetMIME(".xml") + res = GetMIME(".txt") + res = GetMIME(".png") + res = GetMIME(".exe") + res = GetMIME(".json") + } + AssertEqual(b, "application/json", res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = mime.TypeByExtension(".xml") + res = mime.TypeByExtension(".txt") + res = mime.TypeByExtension(".png") + res = mime.TypeByExtension(".exe") + res = mime.TypeByExtension(".json") + } + AssertEqual(b, "application/json", res) + }) +} + +func Test_Utils_StatusMessage(t *testing.T) { + t.Parallel() + res := StatusMessage(204) + AssertEqual(t, "No Content", res) + + res = StatusMessage(404) + AssertEqual(t, "Not Found", res) + + res = StatusMessage(426) + AssertEqual(t, "Upgrade Required", res) + + res = StatusMessage(511) + AssertEqual(t, "Network Authentication Required", res) + + res = StatusMessage(1337) + AssertEqual(t, "", res) + + res = StatusMessage(-1) + AssertEqual(t, "", res) + + res = StatusMessage(0) + AssertEqual(t, "", res) + + res = StatusMessage(600) + AssertEqual(t, "", res) +} + +// go test -v -run=^$ -bench=Benchmark_StatusMessage -benchmem -count=4 +func Benchmark_StatusMessage(b *testing.B) { + var res string + for n := 0; n < b.N; n++ { + res = StatusMessage(http.StatusNotExtended) + } + AssertEqual(b, "Not Extended", res) +} + +func Test_Utils_AssertEqual(t *testing.T) { + t.Parallel() + AssertEqual(nil, []string{}, []string{}) + AssertEqual(t, []string{}, []string{}) +} diff --git a/utils_test.go b/utils_test.go index c224727d..d965e70d 100644 --- a/utils_test.go +++ b/utils_test.go @@ -9,47 +9,72 @@ import ( "testing" "time" - utils "github.com/gofiber/utils" - fasthttp "github.com/valyala/fasthttp" + "github.com/gofiber/fiber/v2/utils" + "github.com/valyala/fasthttp" ) -// go test -v -run=Test_Utils_ -count=3 +// go test -v -run=^$ -bench=Benchmark_Utils_RemoveNewLines -benchmem -count=4 +func Benchmark_Utils_RemoveNewLines(b *testing.B) { + withNL := "foo\r\nSet-Cookie:%20SESSIONID=MaliciousValue\r\n" + withoutNL := "foo Set-Cookie:%20SESSIONID=MaliciousValue " + expected := utils.ImmutableString(withoutNL) + var res string + b.Run("withNewlines", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + res = removeNewLines(withNL) + } + utils.AssertEqual(b, expected, res) + }) + b.Run("withoutNewlines", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + res = removeNewLines(withoutNL) + } + utils.AssertEqual(b, expected, res) + }) + +} + +// go test -v -run=Test_Utils_ -count=3 func Test_Utils_ETag(t *testing.T) { app := New() t.Run("Not Status OK", func(t *testing.T) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) - c.Send("Hello, World!") + c.SendString("Hello, World!") c.Status(201) setETag(c, false) - utils.AssertEqual(t, "", string(c.Fasthttp.Response.Header.Peek(HeaderETag))) + utils.AssertEqual(t, "", string(c.Response().Header.Peek(HeaderETag))) }) t.Run("No Body", func(t *testing.T) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) setETag(c, false) - utils.AssertEqual(t, "", string(c.Fasthttp.Response.Header.Peek(HeaderETag))) + utils.AssertEqual(t, "", string(c.Response().Header.Peek(HeaderETag))) }) t.Run("Has HeaderIfNoneMatch", func(t *testing.T) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) - c.Send("Hello, World!") - c.Fasthttp.Request.Header.Set(HeaderIfNoneMatch, `"13-1831710635"`) + c.SendString("Hello, World!") + c.Request().Header.Set(HeaderIfNoneMatch, `"13-1831710635"`) setETag(c, false) - utils.AssertEqual(t, 304, c.Fasthttp.Response.StatusCode()) - utils.AssertEqual(t, "", string(c.Fasthttp.Response.Header.Peek(HeaderETag))) - utils.AssertEqual(t, "", string(c.Fasthttp.Response.Body())) + utils.AssertEqual(t, 304, c.Response().StatusCode()) + utils.AssertEqual(t, "", string(c.Response().Header.Peek(HeaderETag))) + utils.AssertEqual(t, "", string(c.Response().Body())) }) t.Run("No HeaderIfNoneMatch", func(t *testing.T) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) - c.Send("Hello, World!") + c.SendString("Hello, World!") setETag(c, false) - utils.AssertEqual(t, `"13-1831710635"`, string(c.Fasthttp.Response.Header.Peek(HeaderETag))) + utils.AssertEqual(t, `"13-1831710635"`, string(c.Response().Header.Peek(HeaderETag))) }) } @@ -58,54 +83,83 @@ func Benchmark_Utils_ETag(b *testing.B) { app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) - c.Send("Hello, World!") + c.SendString("Hello, World!") for n := 0; n < b.N; n++ { setETag(c, false) } - utils.AssertEqual(b, `"13-1831710635"`, string(c.Fasthttp.Response.Header.Peek(HeaderETag))) + utils.AssertEqual(b, `"13-1831710635"`, string(c.Response().Header.Peek(HeaderETag))) } +// go test -v -run=Test_Utils_ETag_Weak -count=1 func Test_Utils_ETag_Weak(t *testing.T) { app := New() t.Run("Set Weak", func(t *testing.T) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) - c.Send("Hello, World!") + c.SendString("Hello, World!") setETag(c, true) - utils.AssertEqual(t, `W/"13-1831710635"`, string(c.Fasthttp.Response.Header.Peek(HeaderETag))) + utils.AssertEqual(t, `W/"13-1831710635"`, string(c.Response().Header.Peek(HeaderETag))) }) t.Run("Match Weak ETag", func(t *testing.T) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) - c.Send("Hello, World!") - c.Fasthttp.Request.Header.Set(HeaderIfNoneMatch, `W/"13-1831710635"`) + c.SendString("Hello, World!") + c.Request().Header.Set(HeaderIfNoneMatch, `W/"13-1831710635"`) setETag(c, true) - utils.AssertEqual(t, 304, c.Fasthttp.Response.StatusCode()) - utils.AssertEqual(t, "", string(c.Fasthttp.Response.Header.Peek(HeaderETag))) - utils.AssertEqual(t, "", string(c.Fasthttp.Response.Body())) + utils.AssertEqual(t, 304, c.Response().StatusCode()) + utils.AssertEqual(t, "", string(c.Response().Header.Peek(HeaderETag))) + utils.AssertEqual(t, "", string(c.Response().Body())) }) t.Run("Not Match Weak ETag", func(t *testing.T) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) - c.Send("Hello, World!") - c.Fasthttp.Request.Header.Set(HeaderIfNoneMatch, `W/"13-1831710635xx"`) + c.SendString("Hello, World!") + c.Request().Header.Set(HeaderIfNoneMatch, `W/"13-1831710635xx"`) setETag(c, true) - utils.AssertEqual(t, `W/"13-1831710635"`, string(c.Fasthttp.Response.Header.Peek(HeaderETag))) + utils.AssertEqual(t, `W/"13-1831710635"`, string(c.Response().Header.Peek(HeaderETag))) }) } +func Test_Utils_UniqueRouteStack(t *testing.T) { + route1 := &Route{} + route2 := &Route{} + route3 := &Route{} + utils.AssertEqual( + t, + []*Route{ + route1, + route2, + route3, + }, + uniqueRouteStack([]*Route{ + route1, + route1, + route1, + route2, + route2, + route2, + route3, + route3, + route3, + route1, + route2, + route3, + }), + ) +} + // go test -v -run=^$ -bench=Benchmark_App_ETag_Weak -benchmem -count=4 func Benchmark_Utils_ETag_Weak(b *testing.B) { app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) - c.Send("Hello, World!") + c.SendString("Hello, World!") for n := 0; n < b.N; n++ { setETag(c, true) } - utils.AssertEqual(b, `W/"13-1831710635"`, string(c.Fasthttp.Response.Header.Peek(HeaderETag))) + utils.AssertEqual(b, `W/"13-1831710635"`, string(c.Response().Header.Peek(HeaderETag))) } func Test_Utils_getGroupPath(t *testing.T) { @@ -211,34 +265,6 @@ func Test_Utils_TestConn_Deadline(t *testing.T) { utils.AssertEqual(t, nil, conn.SetWriteDeadline(time.Time{})) } -func Test_Utils_UniqueRouteStack(t *testing.T) { - route1 := &Route{} - route2 := &Route{} - route3 := &Route{} - utils.AssertEqual( - t, - []*Route{ - route1, - route2, - route3, - }, - uniqueRouteStack([]*Route{ - route1, - route1, - route1, - route2, - route2, - route2, - route3, - route3, - route3, - route1, - route2, - route3, - }), - ) -} - func Test_Utils_IsNoCache(t *testing.T) { testCases := []struct { string