-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
6c2e8a6
commit b861d4f
Showing
87 changed files
with
4,368 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
name: list | ||
|
||
on: | ||
push: | ||
tags: | ||
- "v*.*.*" | ||
workflow_dispatch: | ||
jobs: | ||
list: | ||
runs-on: ubuntu-20.04 | ||
steps: | ||
- uses: actions/checkout@v3 | ||
|
||
- uses: actions/setup-go@v4 | ||
with: | ||
go-version-file: 'go.mod' | ||
|
||
- run: go mod download | ||
- run: go test ./... | ||
|
||
- env: | ||
GOPROXY: "proxy.golang.org" | ||
run: go list -m github.com/cardinalby/hureg@${{ github.ref_name }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
name: test | ||
|
||
on: | ||
push: | ||
branches: | ||
- "**" | ||
workflow_dispatch: | ||
pull_request: | ||
jobs: | ||
test: | ||
runs-on: ubuntu-20.04 | ||
steps: | ||
- uses: actions/checkout@v3 | ||
|
||
- uses: actions/setup-go@v4 | ||
with: | ||
go-version-file: 'go.mod' | ||
|
||
- run: go mod download | ||
- run: go test ./... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
vendor | ||
.idea |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,115 @@ | ||
# hmext | ||
Extension of Huma Go framework | ||
 | ||
|
||
[HUMA](https://github.com/danielgtaylor/huma) is a great Go framework that enables you to | ||
expose generated OpenAPI spec in the best way possible. Unfortunately, it lacks some features from other routers. | ||
|
||
This library wraps [HUMA framework](https://github.com/danielgtaylor/huma) endpoints | ||
registration pipeline to provide the missing features: | ||
|
||
### ❤️ Create registration **groups** | ||
|
||
Similar to other routers you can create a derived `api` (i.e. group) that has pre-defined: | ||
- [**Base path**](./docs/base_path.md) (same as `Group`, `Route` methods in other routers) | ||
- [**Multiple**](./docs/base_path.md) alternative **base paths** | ||
- [**Middlewares**](./pkg/huma/op_handler/middlewares.go) | ||
- [**Transformers**](./docs/transformers.md) | ||
- [**Tags**](./pkg/huma/op_handler/add_tags.go) and [other](./pkg/huma/op_handler) Huma Operation properties | ||
that will be applied to all endpoints in a group. | ||
- [**Control**](./docs/reg_middlewares.md) the registration pipeline preventing operation from | ||
registration or registering it multiple times with different properties | ||
|
||
### ❤️ Control over OpenAPI endpoints | ||
|
||
Now you have [manual control](./docs/openapi_endpoints.md) over exposing the spec, docs and schemas: | ||
- Expose only needed spec versions | ||
- Add own middlewares (e.g. authentication to protect the spec on public APIs) | ||
|
||
### ❤️ Access more metadata in Operation Handlers | ||
|
||
The library [provides](./docs/metadata.md) additional information via `Metadata` field to your | ||
own _Operation Handlers_: | ||
- Input/Output types of a handler | ||
- OpenAPI object from `huma.API` instance | ||
- Whether an operation was defined explicitly or implicitly via convenience methods | ||
- etc. | ||
|
||
## Installation | ||
|
||
```bash | ||
go get github.com/cardinalby/hureg | ||
``` | ||
|
||
## Documentation | ||
|
||
### Key concepts | ||
|
||
- [Basic usage](./docs/basic_usage.md) | ||
- [Registration Middlewares](./docs/reg_middlewares.md) | ||
|
||
### Common use-cases | ||
|
||
- [Create a group with base path](./docs/base_path.md) | ||
- [Operation Handlers](./docs/op_handlers.md) | ||
|
||
### Additional features | ||
|
||
- [Operation metadata](./docs/metadata.md) | ||
- [Per-group Transformers](./docs/transformers.md) | ||
- [OpenAPI endpoints](./docs/openapi_endpoints.md) | ||
|
||
## Examples | ||
|
||
### 🔻 Initialization | ||
|
||
```go | ||
import "github.com/cardinalby/hureg" | ||
|
||
chiRouter := chi.NewRouter() // -- | ||
cfg := huma.DefaultConfig("My API", "1.0.0") // default HUMA initialization | ||
humaApi := humachi.New(chiRouter, cfg) // -- | ||
|
||
api := hureg.NewAPIGen(humaApi) // The new line | ||
``` | ||
|
||
### 🔻 "Base path + tags + middlewares" group | ||
|
||
```go | ||
v1gr := api. // all operations registered with v1gr will have: | ||
AddBasePath("/v1"). // - "/v1" base path | ||
AddOpHandler(op_handler.AddTags("some_tag")). // - "some_tag" tag | ||
AddMiddlewares(m1, m2) // - m1, m2 middlewares | ||
|
||
hureg.Get(v1gr, "/cat", ...) // "/v1/cat" with "some_tag" tag and m1, m2 middlewares | ||
hureg.Get(v1gr, "/dog", ...) // "/v1/dog" with "some_tag" tag and m1, m2 middlewares | ||
``` | ||
|
||
### 🔻 Multiple base paths | ||
|
||
Sometimes we need to register the same endpoint with multiple base paths (e.g. `/v1` and `/v2`). | ||
|
||
```go | ||
multiPathGr := api.AddMultiBasePaths(nil, "/v1", "/v2") | ||
|
||
hureg.Get(multiPathGr, "/sparrow", ...) // "/v1/sparrow" | ||
// "/v2/sparrow" | ||
``` | ||
|
||
### 🔻 Transformers per group | ||
|
||
```go | ||
trGr := api.AddTransformers(...) // transformers will be applied only to the operations | ||
// registered in this group | ||
|
||
hureg.Get(trGr, "/crocodile", ...) | ||
``` | ||
|
||
### 🔻 Complete server setup | ||
|
||
Check out [integration_test.go](./integration_test.go) for a complete example of how to use the library: | ||
- create `huma.API` from `chi` router | ||
- create `APIGen` instance on top of `huma.API` | ||
- register operations with `APIGen` instance | ||
- use base paths, tags and _Transformers_ to the groups | ||
- register OpenAPI endpoints manually with Basic Auth middleware | ||
|
||
Uncommenting one line you can run the server and play with it in live mode. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
package hureg | ||
|
||
import ( | ||
"github.com/danielgtaylor/huma/v2" | ||
|
||
"github.com/cardinalby/hureg/pkg/huma/op_handler" | ||
) | ||
|
||
// APIGen is a core type of the library that wraps huma.API and stores RegMiddlewares that should be | ||
// applied to operations before registration in Huma. | ||
// It provides a fluent API to create derived APIGen instances with own set of actions/changes to an | ||
// operation before its registration. | ||
type APIGen struct { | ||
humaAPI huma.API | ||
regMiddlewares RegMiddlewares | ||
transformers []huma.Transformer | ||
} | ||
|
||
// NewAPIGen creates a new APIGen instance with the given huma.API. | ||
func NewAPIGen(humaApi huma.API) APIGen { | ||
return APIGen{ | ||
humaAPI: newHumaApiWrapper(humaApi), | ||
} | ||
} | ||
|
||
// GetHumaAPI returns the wrapped huma.API. | ||
func (a APIGen) GetHumaAPI() huma.API { | ||
return a.humaAPI | ||
} | ||
|
||
// AddRegMiddleware returns a new APIGen instance with the given RegMiddlewares added to the stored RegMiddlewares. | ||
func (a APIGen) AddRegMiddleware(regMiddlewares ...RegMiddleware) APIGen { | ||
a.regMiddlewares = append(a.regMiddlewares, regMiddlewares...) | ||
return a | ||
} | ||
|
||
// GetRegMiddlewares returns the stored RegMiddlewares. | ||
func (a APIGen) GetRegMiddlewares() RegMiddlewares { | ||
return a.regMiddlewares | ||
} | ||
|
||
// AddOpHandler returns a new APIGen instance with the given OperationHandlers added to it. | ||
func (a APIGen) AddOpHandler(handlers ...op_handler.OperationHandler) APIGen { | ||
for _, opHandler := range handlers { | ||
a.regMiddlewares = append(a.regMiddlewares, NewRegMiddleware(opHandler)) | ||
} | ||
return a | ||
} | ||
|
||
// AddTransformers returns a new APIGen instance with the given transformers that will be applied | ||
// to the responses of the handlers registered by this APIGen. | ||
func (a APIGen) AddTransformers(transformers ...huma.Transformer) APIGen { | ||
a.transformers = append(a.transformers, transformers...) | ||
return a | ||
} | ||
|
||
// GetTransformers returns the stored transformers that will be applied to the responses of the handlers | ||
// registered by this APIGen. | ||
func (a APIGen) GetTransformers() []huma.Transformer { | ||
return a.transformers | ||
} | ||
|
||
// AddBasePath returns a new APIGen instance that will add the given basePath segment to an operation`s Path. | ||
// It will append `basePath` to the previously added base paths if any. | ||
// Adding a base path also re-generates the OperationID and Summary fields of an operation if it wasn't explicitly set. | ||
// It's an alternative to Route, Group methods of go routes. | ||
func (a APIGen) AddBasePath(basePath string) APIGen { | ||
return a.AddOpHandler( | ||
op_handler.AddBasePath(basePath), | ||
op_handler.UpdateOperationID(func(op *huma.Operation) string { | ||
// Unlike fan-out, adding base path doesn't create multiple operation registrations | ||
// and doesn't require mandatory operation ID update if it's explicitly provided, | ||
// but generated operation IDs will be updated | ||
return op.OperationID | ||
}), | ||
op_handler.UpdateGeneratedSummary, | ||
) | ||
} | ||
|
||
// AddMultiBasePaths returns a new APIGen instance that will register the same operation with multiple base paths. | ||
// It respects base paths added before and after this method call. | ||
// Since it leads to multiple registrations of the same operation, it requires OperationID to be updated to | ||
// avoid registration of multiple operations with the same OperationID. | ||
// If an operation has generated OperationID, it will be re-generated by default Huma operation ID builder. | ||
// The same is true for the Summary field. | ||
// For the case of explicitly set OperationID, you can provide a custom `explicitOpIDBuilder` builder. | ||
// It will receive an operation with modified Path and metadata.KeyBasePath and should return a new OperationID. | ||
// If `explicitOpIDBuilder` is nil, the built-in approach will be used. It will take metadata.KeyBasePath as | ||
// a prefix for the OperationID, turning it into kebab-case and will append it to the metadata.KeyInitOperationID | ||
// (that stores the initial user-provided OperationID). | ||
func (a APIGen) AddMultiBasePaths( | ||
explicitOpIDBuilder func(*huma.Operation) string, | ||
basePaths ...string, | ||
) APIGen { | ||
basePathsRegMiddlewares := make(RegMiddlewares, len(basePaths)) | ||
for i, basePath := range basePaths { | ||
basePathsRegMiddlewares[i] = NewRegMiddleware( | ||
op_handler.AddBasePath(basePath), | ||
) | ||
} | ||
|
||
return a. | ||
AddRegMiddleware(basePathsRegMiddlewares.FanOut()). | ||
AddOpHandler( | ||
op_handler.UpdateOperationID(explicitOpIDBuilder), | ||
op_handler.UpdateGeneratedSummary, | ||
) | ||
} | ||
|
||
// AddMiddlewares returns a new APIGen instance that will add the given middlewares to the operation. | ||
func (a APIGen) AddMiddlewares(middlewares ...func(huma.Context, func(huma.Context))) APIGen { | ||
return a.AddOpHandler(op_handler.AddMiddlewares(middlewares...)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package hureg | ||
|
||
import ( | ||
"net/http" | ||
"testing" | ||
|
||
"github.com/danielgtaylor/huma/v2" | ||
"github.com/danielgtaylor/huma/v2/adapters/humago" | ||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/cardinalby/hureg/pkg/huma/op_handler" | ||
) | ||
|
||
func newTestApiGen() APIGen { | ||
humaAPI := humago.New(http.NewServeMux(), huma.DefaultConfig("test_api", "1.0.1")) | ||
return NewAPIGen(humaAPI) | ||
} | ||
|
||
func testRegMiddleware( | ||
t *testing.T, | ||
rm RegMiddleware, | ||
op huma.Operation, | ||
testFn func(huma.Operation)) { | ||
wasCalled := false | ||
rm(op, func(op huma.Operation) { | ||
wasCalled = true | ||
testFn(op) | ||
}) | ||
require.True(t, wasCalled) | ||
} | ||
|
||
func TestAPIGen_GetHumaAPI(t *testing.T) { | ||
t.Parallel() | ||
humaAPI := humago.New(http.NewServeMux(), huma.Config{}) | ||
api := NewAPIGen(humaAPI) | ||
require.Equal(t, humaAPI, api.GetHumaAPI()) | ||
} | ||
|
||
func TestAPIGen_GetRegMiddlewares(t *testing.T) { | ||
t.Parallel() | ||
api := newTestApiGen() | ||
rm1 := NewRegMiddleware(op_handler.SetSummary("a", true)) | ||
rm2 := NewRegMiddleware(op_handler.SetSummary("b", true)) | ||
require.Empty(t, api.GetRegMiddlewares()) | ||
derived := api.AddRegMiddleware(rm1, rm2) | ||
require.Len(t, api.GetRegMiddlewares(), 0) | ||
|
||
resRegMiddlewares := derived.GetRegMiddlewares() | ||
require.Len(t, resRegMiddlewares, 2) | ||
testRegMiddleware(t, resRegMiddlewares[0], huma.Operation{}, func(op huma.Operation) { | ||
require.Equal(t, "a", op.Summary) | ||
}) | ||
testRegMiddleware(t, resRegMiddlewares[1], huma.Operation{}, func(op huma.Operation) { | ||
require.Equal(t, "b", op.Summary) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
 | ||
|
||
## Library docs | ||
|
||
### Key concepts | ||
|
||
- [Basic usage](./basic_usage.md) | ||
- [Registration Middlewares](./reg_middlewares.md) | ||
|
||
### Common use-cases | ||
|
||
- [Create a group with base path](./base_path.md) | ||
- [Operation Handlers](./op_handlers.md) | ||
|
||
### Additional features | ||
|
||
- [Extended operation metadata](./metadata.md) | ||
- [Per-group Transformers](./transformers.md) | ||
- [OpenAPI endpoints](./openapi_endpoints.md) |
Oops, something went wrong.