Skip to content

Commit

Permalink
Merge pull request #223 from danielgtaylor/reduce-deps
Browse files Browse the repository at this point in the history
feat: significantly reduce dependencies
  • Loading branch information
danielgtaylor authored Feb 2, 2024
2 parents 9570636 + 7172a41 commit 11b18b0
Show file tree
Hide file tree
Showing 24 changed files with 2,728 additions and 1,172 deletions.
94 changes: 46 additions & 48 deletions adapters/humachi/humachi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import (
"testing"
"time"

humav1 "github.com/danielgtaylor/huma"
"github.com/danielgtaylor/huma/responses"
"github.com/danielgtaylor/huma/v2"
"github.com/go-chi/chi/v5"
)
Expand Down Expand Up @@ -316,49 +314,49 @@ func BenchmarkRawChiFast(b *testing.B) {
}
}

func BenchmarkHumaV1Chi(t *testing.B) {
type GreetingInput struct {
ID string `path:"id"`
ContentType string `header:"Content-Type"`
Num int `query:"num"`
Body struct {
Suffix string `json:"suffix" maxLength:"5"`
}
}

type GreetingOutput struct {
Greeting string `json:"greeting"`
Suffix string `json:"suffix"`
Length int `json:"length"`
ContentType string `json:"content_type"`
Num int `json:"num"`
}

app := humav1.New("My API", "1.0.0")

app.Resource("/foo/{id}").Post("greet", "Get a greeting",
responses.OK().Model(&GreetingOutput{}).Headers("ETag", "Last-Modified"),
).Run(func(ctx humav1.Context, input GreetingInput) {
ctx.Header().Set("ETag", "abc123")
ctx.Header().Set("Last-Modified", lastModified.Format(http.TimeFormat))
resp := &GreetingOutput{}
resp.Greeting = "Hello, " + input.ID + input.Body.Suffix
resp.Suffix = input.Body.Suffix
resp.Length = len(resp.Greeting)
resp.ContentType = input.ContentType
resp.Num = input.Num
ctx.WriteModel(http.StatusOK, resp)
})

reqBody := strings.NewReader(`{"suffix": "!"}`)
req, _ := http.NewRequest(http.MethodPost, "/foo/123?num=5", reqBody)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
t.ResetTimer()
t.ReportAllocs()
for i := 0; i < t.N; i++ {
reqBody.Seek(0, 0)
w.Body.Reset()
app.ServeHTTP(w, req)
}
}
// func BenchmarkHumaV1Chi(t *testing.B) {
// type GreetingInput struct {
// ID string `path:"id"`
// ContentType string `header:"Content-Type"`
// Num int `query:"num"`
// Body struct {
// Suffix string `json:"suffix" maxLength:"5"`
// }
// }

// type GreetingOutput struct {
// Greeting string `json:"greeting"`
// Suffix string `json:"suffix"`
// Length int `json:"length"`
// ContentType string `json:"content_type"`
// Num int `json:"num"`
// }

// app := humav1.New("My API", "1.0.0")

// app.Resource("/foo/{id}").Post("greet", "Get a greeting",
// responses.OK().Model(&GreetingOutput{}).Headers("ETag", "Last-Modified"),
// ).Run(func(ctx humav1.Context, input GreetingInput) {
// ctx.Header().Set("ETag", "abc123")
// ctx.Header().Set("Last-Modified", lastModified.Format(http.TimeFormat))
// resp := &GreetingOutput{}
// resp.Greeting = "Hello, " + input.ID + input.Body.Suffix
// resp.Suffix = input.Body.Suffix
// resp.Length = len(resp.Greeting)
// resp.ContentType = input.ContentType
// resp.Num = input.Num
// ctx.WriteModel(http.StatusOK, resp)
// })

// reqBody := strings.NewReader(`{"suffix": "!"}`)
// req, _ := http.NewRequest(http.MethodPost, "/foo/123?num=5", reqBody)
// req.Header.Set("Content-Type", "application/json")
// w := httptest.NewRecorder()
// t.ResetTimer()
// t.ReportAllocs()
// for i := 0; i < t.N; i++ {
// reqBody.Seek(0, 0)
// w.Body.Reset()
// app.ServeHTTP(w, req)
// }
// }
3 changes: 1 addition & 2 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (
"time"

"github.com/danielgtaylor/huma/v2/negotiation"
"github.com/goccy/go-yaml"
)

var rxSchema = regexp.MustCompile(`#/components/schemas/([^"]+)`)
Expand Down Expand Up @@ -347,7 +346,7 @@ func NewAPI(config Config, a Adapter) API {
}, func(ctx Context) {
ctx.SetHeader("Content-Type", "application/vnd.oai.openapi+yaml")
if specYAML == nil {
specYAML, _ = yaml.Marshal(newAPI.OpenAPI())
specYAML, _ = newAPI.OpenAPI().YAML()
}
ctx.BodyWriter().Write(specYAML)
})
Expand Down
48 changes: 24 additions & 24 deletions cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import (

"github.com/danielgtaylor/casing"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)

// CLI is an optional command-line interface for a Huma service. It is provided
Expand Down Expand Up @@ -75,7 +73,6 @@ type option struct {
type cli[Options any] struct {
root *cobra.Command
optInfo []option
cfg *viper.Viper
onParsed func(Hooks, *Options)
start func()
stop func()
Expand All @@ -88,18 +85,22 @@ func (c *cli[Options]) Run() {
c.root.PersistentPreRun = func(cmd *cobra.Command, args []string) {
// Load config from args/env/files
v := reflect.ValueOf(&o).Elem()
flags := c.root.PersistentFlags()
for _, opt := range c.optInfo {
f := v
for _, i := range opt.path {
f = f.Field(i)
}
switch opt.typ.Kind() {
case reflect.String:
f.Set(reflect.ValueOf(c.cfg.GetString(opt.name)))
s, _ := flags.GetString(opt.name)
f.Set(reflect.ValueOf(s))
case reflect.Int, reflect.Int64:
f.Set(reflect.ValueOf(c.cfg.GetInt64(opt.name)).Convert(opt.typ))
i, _ := flags.GetInt64(opt.name)
f.Set(reflect.ValueOf(i).Convert(opt.typ))
case reflect.Bool:
f.Set(reflect.ValueOf(c.cfg.GetBool(opt.name)))
b, _ := flags.GetBool(opt.name)
f.Set(reflect.ValueOf(b))
}
}

Expand Down Expand Up @@ -130,8 +131,9 @@ func (c *cli[O]) OnStop(fn func()) {
c.stop = fn
}

func (c *cli[O]) setupOptions(flags *pflag.FlagSet, t reflect.Type, path []int) {
func (c *cli[O]) setupOptions(t reflect.Type, path []int) {
var err error
flags := c.root.PersistentFlags()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)

Expand All @@ -147,7 +149,7 @@ func (c *cli[O]) setupOptions(flags *pflag.FlagSet, t reflect.Type, path []int)

if field.Anonymous {
// Embedded struct. This enables composition from e.g. company defaults.
c.setupOptions(flags, deref(field.Type), currentPath)
c.setupOptions(deref(field.Type), currentPath)
continue
}

Expand All @@ -156,35 +158,39 @@ func (c *cli[O]) setupOptions(flags *pflag.FlagSet, t reflect.Type, path []int)
name = casing.Kebab(field.Name)
}

envName := "SERVICE_" + casing.Snake(name, strings.ToUpper)
defaultValue := field.Tag.Get("default")
if v := os.Getenv(envName); v != "" {
// Env vars will override the default value, which is used to document
// what the value is if no options are passed.
defaultValue = v
}

c.optInfo = append(c.optInfo, option{name, field.Type, currentPath})
switch field.Type.Kind() {
case reflect.String:
c.cfg.SetDefault(name, field.Tag.Get("default"))
flags.StringP(name, field.Tag.Get("short"), field.Tag.Get("default"), field.Tag.Get("doc"))
flags.StringP(name, field.Tag.Get("short"), defaultValue, field.Tag.Get("doc"))
case reflect.Int, reflect.Int64:
var def int64
if d := field.Tag.Get("default"); d != "" {
def, err = strconv.ParseInt(d, 10, 64)
if defaultValue != "" {
def, err = strconv.ParseInt(defaultValue, 10, 64)
if err != nil {
panic(err)
}
}
c.cfg.SetDefault(name, def)
flags.Int64P(name, field.Tag.Get("short"), def, field.Tag.Get("doc"))
case reflect.Bool:
var def bool
if d := field.Tag.Get("default"); d != "" {
def, err = strconv.ParseBool(d)
if defaultValue != "" {
def, err = strconv.ParseBool(defaultValue)
if err != nil {
panic(err)
}
}
c.cfg.SetDefault(name, def)
flags.BoolP(name, field.Tag.Get("short"), def, field.Tag.Get("doc"))
default:
panic("Unsupported option type: " + field.Type.Kind().String())
}
c.cfg.BindPFlag(name, flags.Lookup(name))
}
}

Expand Down Expand Up @@ -233,16 +239,10 @@ func NewCLI[O any](onParsed func(Hooks, *O)) CLI {
Use: "myapp",
},
onParsed: onParsed,
cfg: viper.New(),
}

cfg := c.cfg
cfg.SetEnvPrefix("SERVICE")
cfg.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
cfg.AutomaticEnv()

var o O
c.setupOptions(c.root.PersistentFlags(), reflect.TypeOf(o), []int{})
c.setupOptions(reflect.TypeOf(o), []int{})

c.root.Run = func(cmd *cobra.Command, args []string) {
done := make(chan struct{}, 1)
Expand Down
30 changes: 30 additions & 0 deletions cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"log"
"net/http"
"os"
"syscall"
"testing"
"time"
Expand Down Expand Up @@ -88,6 +89,35 @@ func TestCLIPlain(t *testing.T) {
cli.Run()
}

func TestCLIEnv(t *testing.T) {
type Options struct {
Debug bool
Host string
Port int
}

os.Setenv("SERVICE_DEBUG", "true")
os.Setenv("SERVICE_HOST", "localhost")
os.Setenv("SERVICE_PORT", "8001")
defer func() {
os.Unsetenv("SERVICE_DEBUG")
os.Unsetenv("SERVICE_HOST")
os.Unsetenv("SERVICE_PORT")
}()

cli := huma.NewCLI(func(hooks huma.Hooks, options *Options) {
assert.True(t, options.Debug)
assert.Equal(t, "localhost", options.Host)
assert.Equal(t, 8001, options.Port)
hooks.OnStart(func() {
// Do nothing
})
})

cli.Root().SetArgs([]string{})
cli.Run()
}

func TestCLIAdvanced(t *testing.T) {
type DebugOption struct {
Debug bool `doc:"Enable debug mode." default:"false"`
Expand Down
3 changes: 1 addition & 2 deletions docs/docs/features/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ description: Add a CLI to your service for easy configuration and custom command

Huma ships with a built-in lightweight utility to wrap your service with a CLI, enabling you to run it with different arguments and easily write custom commands to do things like print out the OpenAPI or run on-demand database migrations.

The CLI options use a similar strategy to input & output structs, enabling you to use the same pattern for validation and documentation of command line arguments. It uses [Cobra](https://cobra.dev/) & [Viper](https://github.com/spf13/viper) under the hood, enabling automatic environment variable binding and more.
The CLI options use a similar strategy to input & output structs, enabling you to use the same pattern for validation and documentation of command line arguments. It uses [Cobra](https://cobra.dev/) under the hood, enabling custom commands and including automatic environment variable binding and more.

```go title="main.go"
// First, define your input options.
Expand Down Expand Up @@ -170,4 +170,3 @@ If you want to access your custom options struct with custom commands, use the [
- [`huma.API`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#API) the API instance
- External Links
- [Cobra](https://cobra.dev/) CLI library
- [Viper](https://github.com/spf13/viper) Configuration library
24 changes: 24 additions & 0 deletions examples/fields/fields.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Example field selection transform enabling a GraphQL-like behavior.
package fields

import (
"encoding/json"

"github.com/danielgtaylor/huma/v2"
"github.com/danielgtaylor/shorthand/v2"
)

// FieldSelectTransform is an example of a transform that can use an input
// header value to modify the response on the server, providing a GraphQL-like
// way to send only the fields that the client wants over the wire.
func FieldSelectTransform(ctx huma.Context, status string, v any) (any, error) {
if fields := ctx.Header("Fields"); fields != "" {
// Ugh this is inefficient... consider other ways of doing this :-(
var tmp any
b, _ := json.Marshal(v)
json.Unmarshal(b, &tmp)
result, _, err := shorthand.GetPath(fields, tmp, shorthand.GetOptions{})
return result, err
}
return v, nil
}
59 changes: 59 additions & 0 deletions examples/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
module github.com/danielgtaylor/huma/v2/examples

go 1.20

replace github.com/danielgtaylor/huma/v2 => ../

require (
github.com/danielgtaylor/huma v1.14.2
github.com/danielgtaylor/huma/v2 v2.0.0-00010101000000-000000000000
github.com/danielgtaylor/shorthand/v2 v2.2.0
github.com/go-chi/chi v4.1.2+incompatible
github.com/go-chi/chi/v5 v5.0.11
github.com/spf13/cobra v1.8.0
google.golang.org/protobuf v1.32.0
)

require (
github.com/Jeffail/gabs/v2 v2.6.1 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/danielgtaylor/casing v0.0.0-20210126043903-4e55e6373ac3 // indirect
github.com/danielgtaylor/mexpr v1.9.0 // indirect
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/fxamacker/cbor/v2 v2.5.0 // indirect
github.com/goccy/go-yaml v1.11.2 // indirect
github.com/graphql-go/graphql v0.8.0 // indirect
github.com/graphql-go/handler v0.2.3 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/koron-go/gqlcost v0.2.2 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.15.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/tent/http-link-go v0.0.0-20130702225549-ac974c61c2f9 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.21.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading

0 comments on commit 11b18b0

Please sign in to comment.