这是indexloc提供的服务,不要输入任何密码
Skip to content
This repository was archived by the owner on Feb 23, 2022. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions autoconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package huma

// AutoConfigVar represents a variable given by the user when prompted during
// auto-configuration setup of an API.
type AutoConfigVar struct {
Description string `json:"description,omitempty"`
Example string `json:"example,omitempty"`
Default interface{} `json:"default,omitempty"`
Enum []interface{} `json:"enum,omitempty"`
}

// AutoConfig holds an API's automatic configuration settings for the CLI. These
// are advertised via OpenAPI extension and picked up by the CLI to make it
// easier to get started using an API.
type AutoConfig struct {
Security string `json:"security"`
Headers map[string]string `json:"headers,omitempty"`
Prompt map[string]AutoConfigVar `json:"prompt,omitempty"`
Params map[string]string `json:"params"`
}
77 changes: 77 additions & 0 deletions openapi.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package huma

import (
"fmt"
"reflect"

"github.com/istreamlabs/huma/schema"
)

Expand Down Expand Up @@ -41,3 +44,77 @@ type oaParam struct {
// params sent between a load balander / proxy and the service internally.
Internal bool `json:"-"`
}

type oaComponents struct {
Schemas map[string]*schema.Schema `json:"schemas,omitempty"`
SecuritySchemes map[string]oaSecurityScheme `json:"securitySchemes,omitempty"`
}

func (c *oaComponents) AddSchema(t reflect.Type, mode schema.Mode, hint string) string {
// Try to determine the type's name.
name := t.Name()
if name == "" && t.Kind() == reflect.Ptr {
// Take the name of the pointed-to type.
name = t.Elem().Name()
}
if name == "" && t.Kind() == reflect.Slice {
// Take the name of the type in the array and append "List" to it.
tmp := t.Elem()
if tmp.Kind() == reflect.Ptr {
tmp = tmp.Elem()
}
name = tmp.Name()
if name != "" {
name += "List"
}
}
if name == "" {
// No luck, fall back to the passed-in hint. Better than nothing.
name = hint
}

s, err := schema.GenerateWithMode(t, mode, nil)
if err != nil {
panic(err)
}

orig := name
num := 1
for {
if c.Schemas[name] == nil {
// No existing schema, we are the first!
break
}

if reflect.DeepEqual(c.Schemas[name], s) {
// Existing schema matches!
break
}

// If we are here, then an existing schema doesn't match and this is a new
// type. So we will rename it in a deterministic fashion.
num++
name = fmt.Sprintf("%s%d", orig, num)
}

c.Schemas[name] = s

return "#/components/schemas/" + name
}

type oaFlow struct {
AuthorizationURL string `json:"authorizationUrl,omitempty"`
TokenURL string `json:"tokenUrl,omitempty"`
Scopes map[string]string `json:"scopes,omitempty"`
}

type oaFlows struct {
ClientCredentials *oaFlow `json:"clientCredentials,omitempty"`
AuthorizationCode *oaFlow `json:"authorizationCode,omitempty"`
}

type oaSecurityScheme struct {
Type string `json:"type"`
Scheme string `json:"scheme,omitempty"`
Flows oaFlows `json:"flows,omitempty"`
}
48 changes: 48 additions & 0 deletions openapi_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package huma

import (
"reflect"
"testing"

"github.com/istreamlabs/huma/schema"
"github.com/stretchr/testify/assert"
)

type componentFoo struct {
Field string `json:"field"`
Another string `json:"another" readOnly:"true"`
}

type componentBar struct {
Field string `json:"field"`
}

func TestComponentSchemas(t *testing.T) {
components := oaComponents{
Schemas: map[string]*schema.Schema{},
}

// Adding two different versions of the same component.
ref := components.AddSchema(reflect.TypeOf(&componentFoo{}), schema.ModeRead, "hint")
assert.Equal(t, ref, "#/components/schemas/componentFoo")
assert.NotNil(t, components.Schemas["componentFoo"])

ref = components.AddSchema(reflect.TypeOf(&componentFoo{}), schema.ModeWrite, "hint")
assert.Equal(t, ref, "#/components/schemas/componentFoo2")
assert.NotNil(t, components.Schemas["componentFoo2"])

// Re-adding the second should not create a third.
ref = components.AddSchema(reflect.TypeOf(&componentFoo{}), schema.ModeWrite, "hint")
assert.Equal(t, ref, "#/components/schemas/componentFoo2")
assert.Nil(t, components.Schemas["componentFoo3"])

// Adding a list of pointers to a struct.
ref = components.AddSchema(reflect.TypeOf([]*componentBar{}), schema.ModeAll, "hint")
assert.Equal(t, ref, "#/components/schemas/componentBarList")
assert.NotNil(t, components.Schemas["componentBarList"])

// Adding an anonymous empty struct, should use the hint.
ref = components.AddSchema(reflect.TypeOf(struct{}{}), schema.ModeAll, "hint")
assert.Equal(t, ref, "#/components/schemas/hint")
assert.NotNil(t, components.Schemas["hint"])
}
9 changes: 3 additions & 6 deletions operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func newOperation(resource *Resource, method, id, docs string, responses []Respo
}
}

func (o *Operation) toOpenAPI() *gabs.Container {
func (o *Operation) toOpenAPI(components *oaComponents) *gabs.Container {
doc := gabs.New()

doc.Set(o.id, "operationId")
Expand Down Expand Up @@ -98,11 +98,8 @@ func (o *Operation) toOpenAPI() *gabs.Container {
}

if resp.model != nil {
schema, err := schema.GenerateWithMode(resp.model, schema.ModeRead, nil)
if err != nil {
panic(err)
}
doc.Set(schema, "responses", status, "content", resp.contentType, "schema")
ref := components.AddSchema(resp.model, schema.ModeRead, o.id)
doc.Set(ref, "responses", status, "content", resp.contentType, "schema", "$ref")
}
}

Expand Down
6 changes: 3 additions & 3 deletions resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ type Resource struct {
tags []string
}

func (r *Resource) toOpenAPI() *gabs.Container {
func (r *Resource) toOpenAPI(components *oaComponents) *gabs.Container {
doc := gabs.New()

for _, sub := range r.subResources {
doc.Merge(sub.toOpenAPI())
doc.Merge(sub.toOpenAPI(components))
}

for _, op := range r.operations {
opValue := op.toOpenAPI()
opValue := op.toOpenAPI(components)

if len(r.tags) > 0 {
opValue.Set(r.tags, "tags")
Expand Down
98 changes: 84 additions & 14 deletions router.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/Jeffail/gabs/v2"
"github.com/go-chi/chi"
"github.com/istreamlabs/huma/schema"
)

type contextKey string
Expand All @@ -32,13 +33,15 @@ type Router struct {
mux *chi.Mux
resources []*Resource

title string
version string
description string
contact oaContact
servers []oaServer
// securitySchemes
// security
title string
version string
description string
contact oaContact
servers []oaServer
securitySchemes map[string]oaSecurityScheme
security map[string][]string

autoConfig *AutoConfig

// Documentation handler function
docsPrefix string
Expand Down Expand Up @@ -70,9 +73,24 @@ func (r *Router) OpenAPI() *gabs.Container {
doc.Set(r.description, "info", "description")
}

components := &oaComponents{
Schemas: map[string]*schema.Schema{},
SecuritySchemes: r.securitySchemes,
}

paths, _ := doc.Object("paths")
for _, res := range r.resources {
paths.Merge(res.toOpenAPI())
paths.Merge(res.toOpenAPI(components))
}

doc.Set(components, "components")

if len(r.security) > 0 {
doc.Set(r.security, "security")
}

if r.autoConfig != nil {
doc.Set(r.autoConfig, "x-cli-config")
}

if r.openapiHook != nil {
Expand All @@ -97,6 +115,56 @@ func (r *Router) ServerLink(description, uri string) {
})
}

// GatewayBasicAuth documents that the API gateway handles auth using HTTP Basic.
func (r *Router) GatewayBasicAuth(name string) {
r.securitySchemes[name] = oaSecurityScheme{
Type: "http",
Scheme: "basic",
}
}

// GatewayClientCredentials documents that the API gateway handles auth using
// OAuth2 client credentials (pre-shared secret).
func (r *Router) GatewayClientCredentials(name, tokenURL string, scopes map[string]string) {
r.securitySchemes[name] = oaSecurityScheme{
Type: "oauth2",
Flows: oaFlows{
ClientCredentials: &oaFlow{
TokenURL: tokenURL,
Scopes: scopes,
},
},
}
}

// GatewayAuthCode documents that the API gateway handles auth using
// OAuth2 authorization code (user login).
func (r *Router) GatewayAuthCode(name, authorizeURL, tokenURL string, scopes map[string]string) {
r.securitySchemes[name] = oaSecurityScheme{
Type: "oauth2",
Flows: oaFlows{
AuthorizationCode: &oaFlow{
AuthorizationURL: authorizeURL,
TokenURL: tokenURL,
Scopes: scopes,
},
},
}
}

// AutoConfig sets up CLI autoconfiguration via `x-cli-config` for use by CLI
// clients, e.g. using a tool like Restish (https://rest.sh/).
func (r *Router) AutoConfig(autoConfig AutoConfig) {
r.autoConfig = &autoConfig
}

// SecurityRequirement sets up a security requirement for the entire API by
// name and with the given scopes. Use together with the other auth options
// like GatewayAuthCode.
func (r *Router) SecurityRequirement(name string, scopes ...string) {
r.security[name] = scopes
}

// Resource creates a new resource attached to this router at the given path.
// The path can include parameters, e.g. `/things/{thing-id}`. Each resource
// path must be unique.
Expand Down Expand Up @@ -249,12 +317,14 @@ func New(docs, version string) *Router {
title, desc := splitDocs(docs)

r := &Router{
mux: chi.NewRouter(),
resources: []*Resource{},
title: title,
description: desc,
version: version,
servers: []oaServer{},
mux: chi.NewRouter(),
resources: []*Resource{},
title: title,
description: desc,
version: version,
servers: []oaServer{},
securitySchemes: map[string]oaSecurityScheme{},
security: map[string][]string{},
}

r.docsHandler = RapiDocHandler(r)
Expand Down
Loading