Skip to content

Commit

Permalink
Merge pull request #114 from jnichols-git/beta.3
Browse files Browse the repository at this point in the history
Beta.3
  • Loading branch information
jnichols-git authored Dec 12, 2023
2 parents 08c77de + 57438f9 commit 852aacb
Show file tree
Hide file tree
Showing 55 changed files with 631 additions and 1,764 deletions.
58 changes: 5 additions & 53 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,71 +1,23 @@
# Contributing to Matcha

- [Getting Started](#getting-started)
- [Making Changes](#making-changes)
- [Picking an Issue](#picking-an-issue)
- [Creating a Branch for Changes](#creating-a-branch-for-changes)
- [Submitting Changes](#submitting-changes)
- [Submission Standards](#submission-standards)
- [Versioning Policy](#versioning-policy)
- [Deprecated Features](#deprecated-features)

We welcome community contributions to Matcha!

## Getting Started

1. Ensure you have [installed Golang](https://go.dev/dl/) on your development machine. Matcha is currently on version `1.20.2`.
1. Ensure you have [installed Golang](https://go.dev/dl/) on your development machine. Matcha requires at least version 1.20.
2. `git clone [email protected]:jnichols-git/matcha/v2.git`

## Making Changes

### Picking an Issue

We do our best to keep issues up to date and appropriately tagged. Here's a quick rundown on how to pick an issue based on tags.

1. If you're a first time contributor, find one tagged `good first issue` or `patch`. These tend to be short, accessible tasks to help you get to know a specific part of the codebase, like middleware.
2. Once you're more comfortable with the structure of Matcha, pick up `patch`, `minor`, and `bugfix` issues that interest you.
3. If you're feeling a non-code task, there's usually a `documentation` issue or two avaliable for assignment.

Once you have decided on an issue, just leave a comment asking to have it assigned to you. Issues are generally first-come first-serve. If an issue has been inactive for 3 weeks, we will consider reassignment.

### Creating a Branch for Changes

1. Check out the correct development branch. You can find these below in [Versioning Policy](#versioning-policy). `git checkout [version-branch]`
1. Check out the correct development branch (usually `main`).
2. Pull in remote changes by doing a fetch/rebase: `git pull --rebase`
3. Create your own development branch: `git branch [my-branch]`
4. Check out your branch: `git checkout [my-branch]`
5. Make your changes!
6. Push your changes to remote and make a pull request.

Please don't make changes on branch `main` or any semver (`vX.X.X`) branch. It will only make your life harder. If you accidentally commit to one of these, this guide may help (external link): <https://dev.to/projectpage/how-to-move-a-commit-to-another-branch-in-git-4lj4>

### Submitting Changes

1. Push your changes to your personal fork: `git push origin [my-branch]`
2. Make a pull request to the development branch, and request review from a maintainer.
3. Revise your changes based on maintainer feedback and test results.

Once your changes are approved, they'll be squash-and-merged into the feature branch.

## Submission Standards

Currently, new submissions to Matcha are subject to the following criteria:

1. **Performance**: Changes must not significantly decrease performance unless they are urgent bugfixes. End-to-end benchmarks are provided in the docs folder; additionally, if your feature or change is to a system that is integrated heavily, we suggest you add and check benchmarks for it.
2. **Testing**: Test coverage should stay above 95%. New behavior is expected to have associated unit tests, and PRs that drop coverage by more than 2%, or below 90%, will be automatically rejected.
3. **Documentation**: Maintainers may request that you add additional documentation to your code.
4. **Style**: Run `gofmt` on your code.
5. **Zero-Dependency**: Matcha does not use any external libraries. Changes with dependencies will be rejected.

We additionally ask that you avoid the use of AI tools like ChatGPT and GitHub Copilot in your contributions to Matcha. I have concerns regarding the ethics and copyright implications of scraping open-source code for training data, and would prefer that any work submitted be attributable to the users that directly contribute to it.

## Versioning Policy

Matcha's latest stable release is `v1.2.2` (major version 1, minor version 2, patch 2). Releases are tagged in the GitHub repository. We follow these guidelines when deciding if a change is major, minor, or patch:

- Major: The change is non-essential and breaks the existing API. Major changes are not currently being accepted.
- Minor: The change doesn't break the existing API, but changes large portions of the internals of the library, or adds significant functionality to the API. Branch off of, and make pull requests to, branch `v1.3.0`.
- Patch: The change is a bugfix, minor performance improvement, minor API change, or auxilary component (like middleware). Branch off of, and make pull requests to, branch `main`.

### Deprecated Features
## Code Quality

To maintain the long-term health of the project, you/we may elect to *deprecate* features, meaning that we won't support them going forward. If you have a change that overrides old functionality, it's preferred that you mark the old as deprecated and implement alongside it, rather than delete it from the project entirely; we want to keep behavior the same between versions when it's not a bug problem.
Please run `gofmt` and ensure that your code passes tests before you make a PR.
56 changes: 26 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,74 +2,72 @@

[![Coverage Status](https://coveralls.io/repos/github/jnichols-git/matcha/v2/badge.svg?branch=main)](https://coveralls.io/github/jnichols-git/matcha/v2?branch=main)
[![Go Report Card](https://goreportcard.com/badge/github.com/jnichols-git/matcha/v2)](https://goreportcard.com/report/github.com/jnichols-git/matcha/v2)
[![Discord Badge](https://img.shields.io/badge/Join%20us%20on-Discord-blue)](https://discord.gg/gCdJ6NPm)

Matcha is an HTTP router designed for ease of use, power, and extensibility.
Matcha is an HTTP router with lots of features and strong memory performance.

## Features

- Flexible routing--handle your API specifications with ease
- Extensible components for edge cases and integration with 3rd-party tools
- High performance that scales to larger APIs
- Comprehensive and passing test coverage, and extensive benchmarks to track performance on key features
- Easy conversion from standard library; uses stdlib handler signatures and types
- Zero dependencies, zero dependency management
- Match wildcards, regex expressions, and partial routes
- Extend route validation with middleware and requirements
- Performant under load with complex APIs

## Installation

`go get github.com/jnichols-git/matcha/v2@v1.2.2`
`go get github.com/jnichols-git/matcha/v2`

## Basic Usage

Here's a "Hello, World" example to introduce you to Matcha's syntax! It serves requests to `http://localhost:8080/hello`.
You can use `matcha.Router` to create a new Router and `router.HandleFunc` to handle a request path.

```go
package examples
package main

import (
"net/http"
"net/http"

"github.com/jnichols-git/matcha/v2/internal/router"
"github.com/jnichols-git/matcha/v2"
)

func sayHello(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("Hello, World!"))
w.Write([]byte("Hello, World!"))
}

func HelloExample() {
rt := router.Default()
rt.HandleFunc(http.MethodGet, "/hello", sayHello)
// or:
// rt.Handle(http.MethodGet, "/hello", http.HandlerFunc(sayHello))
http.ListenAndServe(":3000", rt)
rt := matcha.Router()
rt.HandleFunc(http.MethodGet, "/hello", sayHello)
http.ListenAndServe(":3000", rt)
}
```

For a step-by-step guide through Matcha's features, see our [User Guide](docs/user-guide.md).
```sh
$ go run ./examples/ hello
$ curl localhost:3000/hello
Hello, World!
$
```

For more features, see our [User Guide](docs/user-guide.md).

## Performance

Matcha has an extensive benchmark suite to help identify, document, and improve performance over time. Additionally, `/bench` contains a comprehensive benchmark API for "MockBoards", a fake website that just so happens to use all of the features of Matcha. The MockBoards API has the following:
To help measure performance, we benchmark performance on a fake API spec for MockBoards.

- 18 distinct endpoints, including
- 4 endpoints requiring authorization using a "client_id" header
- 4 endpoints with an enumeration URI parameter (new/top posts, etc)
- 2 middleware components assigning a request ID and CORS headers
- 1 requirement for target host on all endpoints

The MockBoards benchmarks are run alongside an *offset benchmark* that measures the performance cost of setting up scaffolding
for each request sent to calculate their final score. The values below represent performance numbers that you might expect
to see in practice. Please keep in mind that performance varies by machine--you should run benchmarks on your own
hardware to get a proper idea of how well Matcha's performance suits your needs.
The benchmark checks performance against single sequential requests and bursts of 10 concurrent requests. An "offset" is also calculated for the cost of building requests. The results for each benchmark are `result / count - offset`

### MockBoards API Spec Benchmark
### MockBoards

Benchmark | ns/request | B/request | allocs/request
--- | --- | --- | ---
Sequential | 2226 ns/request | 1909 bytes/request | 27 allocs/request
Concurrent | 1953 ns/request | 1943 bytes/request | 29 allocs/request

### MockBoards Mounted API (v2) Benchmark
### MockBoards with v2

This mounts a copy of the API at `/v2` and runs requests against both the v1 and v2 APIs.

Expand All @@ -78,9 +76,7 @@ Benchmark | ns/request | B/request | allocs/request
Sequential | 2797 ns/request | 2046 bytes/request | 29 allocs/request
Concurrent | 2097 ns/request | 2139 bytes/request | 30 allocs/request

### MockBoards API Routing-Only Benchmark

This is the v1 spec, but with the non-path features stripped out to give a better idea of pure routing costs.
### MockBoards routing-only

Benchmark | ns/request | B/request | allocs/request
--- | --- | --- | ---
Expand Down
24 changes: 24 additions & 0 deletions cors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package matcha

import (
"net/http"

"github.com/jnichols-git/matcha/v2/cors"
)

// SetCORSHeaders sets CORS headers on the response according to your
// cors.Options.
// This function is for managing CORS on "simple" requests that don't use
// preflight. You may also want to try teaware.Options. If you want to handle
// preflight/OPTIONS requests, use Options to create a handler for it.
func SetCORSHeaders(w http.ResponseWriter, req *http.Request, aco *cors.Options) {
cors.SetCORSResponseHeaders(w, req, aco)
}

// Options returns an http.Handler that sets CORS headers according to your
// cors.Options.
func Options(aco *cors.Options) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cors.SetCORSResponseHeaders(w, r, aco)
})
}
119 changes: 119 additions & 0 deletions cors/cors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Package cors defines a set of useful handlers and middleware for setting access control values.
//
// See [https://github.com/jnichols-git/matcha/v2/blob/main/docs/other-features.md#cross-origin-resource-sharing-cors].
package cors

import (
"net/http"
"strconv"
"strings"
)

const (
Origin = string("Origin")
RequestMethod = string("Access-Control-Request-Method")
RequestHeaders = string("Access-Control-Request-Headers")
AllowOrigin = string("Access-Control-Allow-Origin")
AllowMethods = string("Access-Control-Allow-Methods")
AllowHeaders = string("Access-Control-Allow-Headers")
ExposeHeaders = string("Access-Control-Expose-Headers")
MaxAge = string("Access-Control-Max-Age")
AllowCredentials = string("Access-Control-Allow-Credentials")
)

// Request defines the CORS-related fields extracted from an *http.Request.
type Request struct {
Origin string
RequestMethod string
RequestHeaders []string
}

// Get CORS request headers from an HTTP request.
func GetRequest(req *http.Request) (crh *Request) {
crh = &Request{}
crh.Origin = req.Header.Get(Origin)
crh.RequestMethod = req.Header.Get(RequestMethod)
if len(crh.RequestMethod) == 0 {
crh.RequestMethod = req.Method
}
crh.RequestHeaders = req.Header.Values(RequestHeaders)
return
}

// Options define the set of options that a CORS request may return.
type Options struct {
AllowOrigin []string
AllowMethods []string
AllowHeaders []string
ExposeHeaders []string
MaxAge float64
AllowCredentials bool
}

// ReflectRequest sets headers in out depending on the provided Options and
// Request.
// To limit the cost of OPTIONS requests, ReflectRequest sends the minimum
// possible permissions.
func ReflectRequest(aco *Options, crh *Request, out http.Header) {
/*
out := &Options{
AllowHeaders: make([]string, len(crh.RequestHeaders)),
ExposeHeaders: make([]string, len(aco.ExposeHeaders)),
MaxAge: 0,
AllowCredentials: aco.AllowCredentials,
}
*/
if len(aco.AllowOrigin) == 1 && aco.AllowOrigin[0] == "*" {
out.Set(AllowOrigin, crh.Origin)
} else {
for _, allowedOrigin := range aco.AllowOrigin {
if crh.Origin == allowedOrigin {
out.Set(AllowOrigin, crh.Origin)
break
}
}
}
if len(aco.AllowMethods) == 1 && aco.AllowMethods[0] == "*" {
out.Set(AllowMethods, crh.RequestMethod)
} else {
for _, allowedMethod := range aco.AllowMethods {
if crh.RequestMethod == allowedMethod {
out.Set(AllowMethods, crh.RequestMethod)
break
}
}
}
if len(aco.AllowHeaders) == 1 && aco.AllowHeaders[0] == "*" {
for _, requestedHeader := range crh.RequestHeaders {
out.Add(AllowHeaders, requestedHeader)
}
} else {
allowed:
for _, allowedHeader := range aco.AllowHeaders {
for _, requestedHeader := range crh.RequestHeaders {
if strings.EqualFold(allowedHeader, requestedHeader) {
out.Add(AllowHeaders, requestedHeader)
continue allowed
}
}
}
}
// There's not a great way to check which headers need to be exposed, so this is returned as * if that's provided.
for _, exposedHeader := range aco.ExposeHeaders {
out.Add(ExposeHeaders, exposedHeader)
}
out.Set(MaxAge, strconv.FormatFloat(aco.MaxAge, 'f', 0, 64))
out.Set(AllowCredentials, strconv.FormatBool(aco.AllowCredentials))
}

// Updates the response headers from http.ResponseWriter to mirror a set of access control options.
// Mirroring provides the minimum amount of permissions needed for the inbound request via ReflectCorsRequestHeaders.
func SetCORSResponseHeaders(w http.ResponseWriter, req *http.Request, aco *Options) {
crh := GetRequest(req)
h := w.Header()
h.Del(AllowOrigin)
h.Del(AllowMethods)
h.Del(AllowHeaders)
h.Del(ExposeHeaders)
ReflectRequest(aco, crh, h)
}
Loading

0 comments on commit 852aacb

Please sign in to comment.