Skip to content

Commit

Permalink
init commit
Browse files Browse the repository at this point in the history
  • Loading branch information
cardinalby committed Jun 25, 2024
1 parent 6c2e8a6 commit b861d4f
Show file tree
Hide file tree
Showing 87 changed files with 4,368 additions and 2 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/list.yml
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 }}
20 changes: 20 additions & 0 deletions .github/workflows/test.yml
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 ./...
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
vendor
.idea
117 changes: 115 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,115 @@
# hmext
Extension of Huma Go framework
![hureg logo](./docs/hureg.png)

[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.
113 changes: 113 additions & 0 deletions api_gen.go
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...))
}
56 changes: 56 additions & 0 deletions api_gen_test.go
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)
})
}
19 changes: 19 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
![hureg logo](./hureg.png)

## 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)
Loading

0 comments on commit b861d4f

Please sign in to comment.